diff --git a/.circleci/config.yml b/.circleci/config.yml index 7446862c6..b6a6ba78e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,39 +1,103 @@ version: 2 jobs: - build: + test-debug: docker: - image: circleci/android:api-28 - working_directory: ~/AntennaPod - environment: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms256m -Xmx1280m" - steps: - checkout - - restore_cache: keys: - v1-android-{{ checksum "build.gradle" }} - # fallback to using the latest cache if no exact match is found - v1-android- - - run: - # To build release, we need to create a temporary keystore that can be used to sign the app - command: | - keytool -noprompt -genkey -v -keystore "app/keystore" -alias alias -storepass password -keypass password -keyalg RSA -validity 10 -dname "CN=antennapod.org, OU=dummy, O=dummy, L=dummy, S=dummy, C=US" - ./gradlew assembleRelease :core:testPlayReleaseUnitTest -PdisablePreDex - no_output_timeout: 1800 - - - store_artifacts: - path: app/build/outputs/apk - destination: apks - + name: Build debug + command: ./gradlew assembleDebug -PdisablePreDex + - run: + name: Execute debug unit tests + command: ./gradlew :core:testPlayDebugUnitTest -PdisablePreDex - save_cache: paths: - ~/.android - ~/.gradle - ~/android key: v1-android-{{ checksum "build.gradle" }} + + test-release: + docker: + - image: circleci/android:api-28 + working_directory: ~/AntennaPod + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms256m -Xmx1280m" + steps: + - checkout + - restore_cache: + keys: + - v1-android-{{ checksum "build.gradle" }} + - v1-android- + - run: + name: Create temporary release keystore + command: keytool -noprompt -genkey -v -keystore "app/keystore" -alias alias -storepass password -keypass password -keyalg RSA -validity 10 -dname "CN=antennapod.org, OU=dummy, O=dummy, L=dummy, S=dummy, C=US" + - run: + name: Build release + command: ./gradlew assembleRelease -PdisablePreDex + - run: + name: Execute release unit tests + command: ./gradlew :core:testPlayReleaseUnitTest -PdisablePreDex + - save_cache: + paths: + - ~/.android + - ~/.gradle + - ~/android + key: v1-android-{{ checksum "build.gradle" }} + + build-androidtest: + docker: + - image: circleci/android:api-28 + working_directory: ~/AntennaPod + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms256m -Xmx1280m" + steps: + - checkout + - restore_cache: + keys: + - v1-android-{{ checksum "build.gradle" }} + - v1-android- + - run: + name: Build integration tests + command: ./gradlew :app:assemblePlayDebugAndroidTest -PdisablePreDex + - save_cache: + paths: + - ~/.android + - ~/.gradle + - ~/android + key: v1-android-{{ checksum "build.gradle" }} + + checkstyle: + docker: + - image: circleci/android:api-28 + working_directory: ~/AntennaPod + steps: + - checkout + - run: + name: Checkstyle + command: ./gradlew checkstyle + +workflows: + version: 2 + + unit-tests: + jobs: + - test-debug + - test-release + - build-androidtest + + static-analysis: + jobs: + - checkstyle diff --git a/.tx/.gitignore b/.tx/.gitignore new file mode 100644 index 000000000..01b481584 --- /dev/null +++ b/.tx/.gitignore @@ -0,0 +1,2 @@ +antennapod.description +antennapod.shortdescription diff --git a/.tx/antennapod.description/pt_translation b/.tx/antennapod.description/pt_translation deleted file mode 100644 index a5d85c553..000000000 --- a/.tx/antennapod.description/pt_translation +++ /dev/null @@ -1,43 +0,0 @@ -O AntennaPod é um gestor de podcasts que lhe permite aceder a milhões de podcasts, gratuitos ou pagos, a partir de diversas fontes tais como as estações BBC, NPR e CNN. A adição, importação e exportação de fontes é muito fácil através da base de dados iTunes ou gPodder, ficheiros OPML ou fontes RSS. Poupe tempo, economize bateria e dados móveis através dos mecanismos de controlo de descargas de episódios (possibilidade de especificar intervalos ou horas para as descargas e redes Wifi) e de eliminação de episódios (de acordo com as suas preferências).
-Mas ainda mais importante: pode descarregar, emitir ou colocar episódios na lista de reprodução ao seu gosto, pode utilizar velocidades variáveis de reprodução, tem suporte a capítulos e um temporizador. Pode também mostrar o seu apreço aos criadores dos episódios através do serviço Flattr. - -Criado por entusiastas de podcasts, o AntennaPod é livre em todos os sentidos da palavra: open source, gratuito e sem publicidade. - -Funcionalidades:
-Importação, organização e reprodução
-• Adicione e importe fontes existentes nos diretórios iTunes e gPodder.net, ficheiros OPML e ligações ATOM e RSS
-• Gestão de podcasts através do widget, barra de notificações e controlos de auriculares ou auscultadores
-• Velocidade variável de reprodução, suporte a capítulos (MP3, VorbisComment e Podlove), memorização da posição de reprodução e um temporizador avançado (agite para repor, baixar e aumentar o volume)
-• Acesso a fontes e episódios protegidos por palavra-passe
-• Possibilidade de subscrever fontes paginadas (www.podlove.org/paged-feeds) - -Monitorização, partilha e suporte
-• Monitorize os seus podcasts preferidos marcando-os como favoritos
-• Localize um episódio através do histórico de reprodução ou através de uma pesquisa (títulos e notas)
-• Partilhe episódios e fontes nas redes sociais, por e-mail, no diretório gPodder.net ou através de ficheiros OPML
-• Ajude os criadores de conteúdos através do serviço Flattr - -Controlo do sistema
-• Controle todas as descargas automáticas: escolha as fontes, exclua redes móveis, especifique as redes Wi-Fi, indique se o telefone deve estar a ser carregado e defina as horas ou intervalos das descargas
-• Faça a gestão do armazenamento através da cache de episódios, da eliminação inteligente (de acordo com os seus favoritos e estado de reprodução) e selecionado a localização de armazenamento
-• Utilize o AntennaPod no seu idioma (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)
-• Adapte-se ao seu ambiente através dos temas claro ou escuro
-• Salvaguarde as suas subscrições com a integração gPodder.net ou através da exportação OPML - -Integre a comunidade do AntennaPod!
-O AntennaPod é desenvolvido por voluntários. Você também pode contribuir na programação ou reportando os erros encontrados! - -O GitHub é o local certo para os pedidos de funcionalidades, relatórios de erros e contributos:
-https://www.github.com/AntennaPod/AntennaPod - -O nosso grupo Google é o local certo para partilhar ideias e agradecer aos nossos voluntários:
-https://groups.google.com/forum/#!forum/antennapod - -Tem alguma questão ou comentário a fazer? -https://twitter.com/@AntennaPod - -O Transifex é o local no qual pode ajudar a traduzir a aplicação:
-https://www.transifex.com/antennapod/antennapod - -Junte-se ao nosso programa Beta para obter as funcionalidades mais recentes:
-https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod \ No newline at end of file diff --git a/.tx/antennapod.shortdescription/pt_translation b/.tx/antennapod.shortdescription/pt_translation deleted file mode 100644 index c558abaca..000000000 --- a/.tx/antennapod.shortdescription/pt_translation +++ /dev/null @@ -1 +0,0 @@ -Gestor e reprodutor de podcasts simples, flexível e open souce \ No newline at end of file diff --git a/.tx/config b/.tx/config index 110f2ba71..ea1c0ccef 100644 --- a/.tx/config +++ b/.tx/config @@ -24,7 +24,7 @@ trans.gl = core/src/main/res/values-gl-rES/strings.xml trans.he_IL = core/src/main/res/values-iw-rIL/strings.xml trans.hi_IN = core/src/main/res/values-hi-rIN/strings.xml trans.hu = core/src/main/res/values-hu/strings.xml -trans.id = core/src/main/res/values-id/strings.xml +trans.id = core/src/main/res/values-in/strings.xml trans.it_IT = core/src/main/res/values-it/strings.xml trans.is = core/src/main/res/values-is-rIS/strings.xml trans.ja = core/src/main/res/values-ja/strings.xml @@ -70,10 +70,13 @@ trans.el = app/src/main/play/listings/el-GR/full-description.txt trans.es = app/src/main/play/listings/es-ES/full-description.txt trans.et = app/src/main/play/listings/et/full-description.txt trans.fa = app/src/main/play/listings/fa/full-description.txt +trans.fi = app/src/main/play/listings/fi-FI/full-description.txt trans.fr = app/src/main/play/listings/fr-FR/full-description.txt trans.gl = app/src/main/play/listings/gl-ES/full-description.txt trans.hi_IN = app/src/main/play/listings/hi-IN/full-description.txt +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 @@ -107,10 +110,13 @@ trans.el = app/src/main/play/listings/el-GR/short-description.txt trans.es = app/src/main/play/listings/es-ES/short-description.txt trans.et = app/src/main/play/listings/et/short-description.txt trans.fa = app/src/main/play/listings/fa/short-description.txt +trans.fi = app/src/main/play/listings/fi-FI/short-description.txt trans.fr = app/src/main/play/listings/fr-FR/short-description.txt trans.gl = app/src/main/play/listings/gl-ES/short-description.txt trans.hi_IN = app/src/main/play/listings/hi-IN/short-description.txt +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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e966b997..fb429f026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ Change Log ========== +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 diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 6cc7ae30e..c333a44b1 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,6 +6,7 @@ Alexander Terczka Alexei Bendebury Ali alifeflow +alimemonzx amhokies Anders Bo Rasmussen Anderson Mesquita @@ -13,8 +14,10 @@ Andrew Gaul Andrey Krutov Anthony Lieuallen axq +bhlee Borjan Tchakaloff brad +Brad Pitcher Burt Wiley Snyder ByteHamster Cameron Banga @@ -23,6 +26,7 @@ Christopher Szucko Cj Malone Colin Willson Cédric Cabessa +damoasda Danial Klimkin Daniel Oeh David Carver @@ -48,9 +52,11 @@ Humberto Fraga InsidE James Falcon Jan Niehusmann +Jan-Peter von Hunnius Jatin Kumar Jens Klingenberg Jens Müller +Joe Stein Johan Liesén Kaligule Katrin Leinweber @@ -93,6 +99,7 @@ qkolj Raghul Raghul Jagannathan recalculated +rezanejati Ross Harrison Sam Lee Sam Whited @@ -109,6 +116,7 @@ Simon Schubert Soso Tughushi Spencer Visick Stefan Mitrik +Stephen Kitt Terence Eden Tim Butram Tobias Preuss @@ -121,6 +129,7 @@ Udi Finkelstein VarunBarad volhol Volker Hollich +Wagubi Brian WangYun William Seemann ydinath @@ -136,47 +145,48 @@ Bulgarian: solusitor Catalan: dvd1985, exort12, javiercoll, lambdani, marcmetallextrem, xc70 Catalan (Spain): 00c0c0, javiercoll Chinese: dillonbecker, RainSlide, xukeek, yangyang -Chinese (China): bebeauties38, domingos86, dudeG, ErlichLiu, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, kavdx, kyleehee, linxiangyu, molisiye, owen8877, RainSlide, stellaxuyi, tupunco, wi24rd, wongsyrone, xukeek, yangyang, yiqiok, YogaGuru +Chinese (China): bebeauties38, domingos86, dudeG, ErlichLiu, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, JayYoung, jhxie, kavdx, kyleehee, linxiangyu, molisiye, owen8877, RainSlide, Sak94664, spice2wolf, stellaxuyi, tupunco, wi24rd, wongsyrone, xukeek, yangyang, yiqiok, YogaGuru Chinese (Taiwan): gugod, nigelinux, pggdt, ymhuang0808 Czech (Czech Republic): elich, Hanzmeister, mcepl, petnek, svetlemodry Danish: CasperHN, jhertel Dutch: e2jk, glotzbach, rwv, Vistaus English: mfietz, sterylmreep Estonian: Eraser -Finnish: danieloeh -French: cactux, ChaoticMind, clombion, e2jk, lacouture, Matth78, mfietz, Poussinou, PRIMOKORN, repat, Sioul, sterylmreep, TacoTheDank, Tilwa, vcariven, whenrow +Finnish: danieloeh, elguitar, Sahtor +French: cactux, ChaoticMind, clombion, e2jk, lacouture, LouFex, Matth78, mfietz, Poussinou, PRIMOKORN, repat, Sioul, sterylmreep, TacoTheDank, Tilwa, vcariven, whenrow Galician: antiparvos, pikamoku, Raichely -German: 112358, altegedanken, barilla, benedikt.g, bitsunited, Buggi, ceving, ChaoticMind, Chaquotay, dab0015, dadosch, DerSilly, die_otto, DJaeger, elkangaroo, enz, fidel, finsterwalder, Foso, GNi33, hightower5, HolgerJeromin, kalei, lohmann, LostInWeb, mfietz, nilso, repat, SAPlayer, schafia, Schroedingberg, sevenmaster, sucaml, Teaspoon, theonlytruth, weltenwort, Wyrrrd, ypid +German: 112358, altegedanken, barilla, benedikt.g, bitsunited, Buggi, ceving, ChaoticMind, Chaquotay, dab0015, dadosch, DerSilly, die_otto, DJaeger, elkangaroo, enz, fidel, finsterwalder, Foso, GNi33, hightower5, HolgerJeromin, kalei, lohmann, LostInWeb, mfietz, moasda, nilso, repat, SAPlayer, schafia, Schroedingberg, sevenmaster, sucaml, Teaspoon, theonlytruth, weltenwort, Wyrrrd, ypid Greek: antonist, danieloeh, hua2016s, MSavoritias, pavlosv Hebrew (Israel): amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron, הלוי11 Hindi (India): ankitiitb1069, Isaasu, nmabhinandan, purple.coder, siddhusengar -Hungarian: glatz.balazs, lna91, marthynw, naren93, tszauer, ttyborg42 +Hungarian: glatz.balazs, hurrikan, lna91, marthynw, naren93, tszauer, ttyborg42 Icelandic: marthjod -Indonesian: jff, luke137, rezafaiza, silvanael16 +Indonesian: jff, levirs565, luke137, rezafaiza, silvanael16 Italian: aalex70, allin, apanontin, Bonnee, giuseppep, Guybrush88, marco_pag, neonsoftware, sevenmaster, theloca95 -Italian (Italy): aalex70, allin, apanontin, Bonnee, buongiorgio, giuseppep, Guybrush88, m.chinni, neonsoftware, nixxo, sevenmaster, theloca95 +Italian (Italy): aalex70, allin, apanontin, Bonnee, buongiorgio, giuseppep, Guybrush88, m.chinni, neonsoftware, niccord, nixxo, sevenmaster, theloca95 Japanese: Naofumi, RACER1, sh3llc4t, TranslatorG Kannada (India): thejeshgn -Korean: changwoo, seungrye, skcha +Korean: changwoo, libliboom, seungrye, skcha Korean (South Korea): changwoo, seungrye Lithuanian: naglis Macedonian: krisfremen -Malayalam: rashivkp +Malayalam: rashivkp, rubenroy Norwegian: timbast Norwegian Bokmål: corkie, danieloeh, heraldo Norwegian Bokmål (Norway): corkie, heraldo, kongk, timbast -Persian: ahangarha, F7D +Persian: ahangarha, F7D, sinamoghaddas Polish: Iwangelion, maniexx, mateossh, mfloryan Polish (Poland): d6210809, hiro2020, Iwangelion, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle Portuguese: andersonvom, domingos86, emansije, smarquespt Portuguese (Brazil): alexupits, alysonborges, andersonvom, arua, caioau, carlo_valente, castrors, deandreamatias, edman, Firmino, jackmiras, Junin, lipefire, lluccia, lucasmotacr, mbaltar, rogervezaro, RubeensVinicius, SamWilliam, silvanael16 Romanian (Romania): corneliu.e, fuzzmz, ralienpp -Russian (Russia): astra1, btimofeev, Duke_Raven, GaynullinDima, MegMasters98, mercutiy, null, overmind88, s.chebotar, shams4real, skvheadless, un_logic, whereisthetea, zhenya97 +Russian (Russia): astra1, btimofeev, Duke_Raven, gammja, GaynullinDima, MegMasters98, mercutiy, null, overmind88, PtilopsisLeucotis, s.chebotar, shams4real, skvheadless, un_logic, Vladryyu, whereisthetea, zhenya97 Slovenian (Slovenia): panter23 Spanish: AleksSyntek, andersonvom, coperfix, deandreamatias, domingos86, dvd1985, Fitoschido, frandavid100, hard_ware, javiercoll, Juanmuto, lambdani, LatinSuD, leogrignafini, palopezv, TacoTheDank, tres.14159, wakutiteo Spanish (Spain): andersonvom, dvd1985, e2jk, frandavid100, hard_ware, palopezv, Raichely, TacoTheDank +Swahili: kmtra Swahili (Kenya): BonfaceKilz -Swedish (Sweden): albin.brantin, Bio, bpnilsson, ChaoticMind, jony08, nilso, SharpMelon, TwoD +Swedish (Sweden): albin.brantin, Bio, bpnilsson, ChaoticMind, jony08, nilso, SharpMelon, TiloWiklund, TwoD Telugu: Isaasu, veeven Turkish: basarancaner, brsata, Erdy, golcuk, overbite, Slsdem Ukrainian (Ukraine): older, paul_sm, sergiyr, zhenya97 diff --git a/app/build.gradle b/app/build.gradle index 46b2e01d7..3996d7ba5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,10 +1,9 @@ plugins { - id('com.github.triplet.play') version '2.2.0' + id('com.android.application') + id('com.getkeepsafe.dexcount') + id('com.github.triplet.play') version '2.3.0' apply false } -apply plugin: "com.android.application" -apply plugin: 'com.getkeepsafe.dexcount' - import org.apache.tools.ant.filters.ReplaceTokens android { @@ -19,8 +18,8 @@ android { // "1.2.3-SNAPSHOT" -> 1020300 // "1.2.3-RC4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 1070296 - versionName "1.7.2b" + versionCode 1070396 + versionName "1.7.3b" testApplicationId "de.test.antennapod" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" generatedDensities = [] @@ -173,9 +172,9 @@ dependencies { implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" implementation 'com.github.mfietz:fyydlin:v0.4.2' - implementation 'com.github.ByteHamster:SearchPreference:v1.2.6' - implementation "org.awaitility:awaitility:$awaitilityVersion" + implementation 'com.github.ByteHamster:SearchPreference:v1.3.0' + androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" androidTestImplementation 'com.nanohttpd:nanohttpd-webserver:2.1.1' androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion" androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' @@ -185,18 +184,12 @@ dependencies { androidTestImplementation 'com.android.support.test:rules:1.0.2' } -play { - track = 'alpha' - - if (project.hasProperty("antennaPodServiceAccountEmail")) { +if (project.hasProperty("antennaPodServiceAccountEmail")) { + apply plugin: 'com.github.triplet.play' + play { + track = 'alpha' serviceAccountEmail = antennaPodServiceAccountEmail - } else { - serviceAccountEmail = '522080222319-compute@developer.gserviceaccount.com' - } - if (project.hasProperty("antennaPodPk12File")) { serviceAccountCredentials = file(antennaPodPk12File) - } else { - serviceAccountCredentials = file('../serviceaccount-c3d7d0f61387.p12') } } diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index 50109c71a..9e86275fc 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -1,6 +1,7 @@ package de.test.antennapod; import android.content.Context; +import android.content.SharedPreferences; import android.support.annotation.StringRes; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.PerformException; @@ -12,6 +13,10 @@ import android.support.test.espresso.util.HumanReadables; import android.support.test.espresso.util.TreeIterables; import android.view.View; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.dialog.RatingDialog; +import de.danoeh.antennapod.fragment.QueueFragment; import org.hamcrest.Matcher; import java.io.File; @@ -74,7 +79,7 @@ public class EspressoTestUtils { /** * Clear all app databases */ - public static void clearAppData() { + public static void clearPreferences() { File root = InstrumentationRegistry.getTargetContext().getFilesDir().getParentFile(); String[] sharedPreferencesFileNames = new File(root, "shared_prefs").list(); for (String fileName : sharedPreferencesFileNames) { @@ -84,6 +89,31 @@ public class EspressoTestUtils { } } + public static void makeNotFirstRun() { + InstrumentationRegistry.getTargetContext().getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false) + .commit(); + + RatingDialog.init(InstrumentationRegistry.getTargetContext()); + RatingDialog.saveRated(); + } + + public static void setLastNavFragment(String tag) { + InstrumentationRegistry.getTargetContext().getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putString(MainActivity.PREF_LAST_FRAGMENT_TAG, tag) + .commit(); + } + + public static void clearDatabase() { + PodDBAdapter.init(InstrumentationRegistry.getTargetContext()); + PodDBAdapter.deleteDatabase(); + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.close(); + } + public static void clickPreference(@StringRes int title) { onView(withId(R.id.list)).perform( RecyclerViewActions.actionOnItem(hasDescendant(withText(title)), diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java index 63ea697e9..d1ca25ad3 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java @@ -64,15 +64,11 @@ public class PlaybackServiceMediaPlayerTest { @Before public void setUp() throws Exception { assertionError = null; - EspressoTestUtils.clearAppData(); - final Context context = InstrumentationRegistry.getTargetContext(); + EspressoTestUtils.clearPreferences(); + EspressoTestUtils.makeNotFirstRun(); + EspressoTestUtils.clearDatabase(); - // create new database - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); + final Context context = InstrumentationRegistry.getTargetContext(); httpServer = new HTTPBin(); httpServer.start(); 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 d013ab9cf..0c65bb2e6 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -5,8 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.support.test.InstrumentationRegistry; -import android.support.test.espresso.intent.Intents; -import android.support.test.rule.ActivityTestRule; +import android.support.test.espresso.intent.rule.IntentsTestRule; import android.support.test.runner.AndroidJUnit4; import com.robotium.solo.Solo; import com.robotium.solo.Timeout; @@ -29,7 +28,6 @@ import static android.support.test.InstrumentationRegistry.getInstrumentation; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.contrib.ActivityResultMatchers.hasResultCode; -import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.clickPreference; import static de.test.antennapod.EspressoTestUtils.openNavDrawer; @@ -45,47 +43,28 @@ public class MainActivityTest { private Solo solo; private UITestUtils uiTestUtils; - private SharedPreferences prefs; @Rule - public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); + public IntentsTestRule mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false); @Before public void setUp() throws IOException { - // override first launch preference - // do this BEFORE calling getActivity()! - EspressoTestUtils.clearAppData(); - prefs = InstrumentationRegistry.getContext() - .getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE); - prefs.edit().putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false).commit(); + EspressoTestUtils.clearPreferences(); + EspressoTestUtils.makeNotFirstRun(); + EspressoTestUtils.clearDatabase(); mActivityRule.launchActivity(new Intent()); - Intents.init(); - Context context = mActivityRule.getActivity(); - uiTestUtils = new UITestUtils(context); + uiTestUtils = new UITestUtils(InstrumentationRegistry.getTargetContext()); uiTestUtils.setup(); - // create new database - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - - RatingDialog.init(context); - RatingDialog.saveRated(); - solo = new Solo(getInstrumentation(), mActivityRule.getActivity()); } @After public void tearDown() throws Exception { uiTestUtils.tearDown(); - solo.finishOpenedActivities(); - Intents.release(); PodDBAdapter.deleteDatabase(); - prefs.edit().clear().commit(); } @Test @@ -93,13 +72,10 @@ public class MainActivityTest { uiTestUtils.addHostedFeedData(); final Feed feed = uiTestUtils.hostedFeeds.get(0); openNavDrawer(); - solo.clickOnText(solo.getString(R.string.add_feed_label)); + onView(withText(R.string.add_feed_label)).perform(click()); solo.enterText(1, feed.getDownload_url()); - solo.clickOnButton(solo.getString(R.string.confirm_label)); - solo.waitForActivity(OnlineFeedViewActivity.class); - solo.waitForView(R.id.butSubscribe); - assertEquals(solo.getString(R.string.subscribe_label), solo.getButton(0).getText().toString()); - solo.clickOnButton(0); + onView(withText(R.string.confirm_label)).perform(click()); + onView(withText(R.string.subscribe_label)).perform(click()); assertTrue(solo.waitForText(solo.getString(R.string.open_podcast), 0, Timeout.getLargeTimeout(), false)); } @@ -119,7 +95,6 @@ public class MainActivityTest { onView(withText(R.string.confirm_label)).perform(click()); solo.goBackToActivity(MainActivity.class.getSimpleName()); - solo.goBack(); // Close nav drawer solo.goBack(); assertEquals(solo.getString(R.string.subscriptions_label), getActionbarTitle()); } @@ -132,7 +107,6 @@ public class MainActivityTest { clickPreference(R.string.pref_back_button_behavior_title); onView(withText(R.string.back_button_open_drawer)).perform(click()); solo.goBackToActivity(MainActivity.class.getSimpleName()); - solo.goBack(); // Close nav drawer solo.goBack(); assertTrue(((MainActivity)solo.getCurrentActivity()).isDrawerOpen()); } @@ -145,7 +119,6 @@ public class MainActivityTest { clickPreference(R.string.pref_back_button_behavior_title); onView(withText(R.string.back_button_double_tap)).perform(click()); solo.goBackToActivity(MainActivity.class.getSimpleName()); - solo.goBack(); // Close nav drawer solo.goBack(); solo.goBack(); assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); @@ -159,7 +132,6 @@ public class MainActivityTest { clickPreference(R.string.pref_back_button_behavior_title); onView(withText(R.string.back_button_show_prompt)).perform(click()); solo.goBackToActivity(MainActivity.class.getSimpleName()); - solo.goBack(); // Close nav drawer solo.goBack(); onView(withText(R.string.yes)).perform(click()); Thread.sleep(100); @@ -174,7 +146,6 @@ public class MainActivityTest { clickPreference(R.string.pref_back_button_behavior_title); onView(withText(R.string.back_button_default)).perform(click()); solo.goBackToActivity(MainActivity.class.getSimpleName()); - solo.goBack(); // Close nav drawer solo.goBack(); assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java b/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java index 0ed62010b..548843636 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java @@ -4,13 +4,11 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.ViewInteraction; import android.support.test.espresso.contrib.DrawerActions; -import android.support.test.espresso.intent.Intents; -import android.support.test.filters.FlakyTest; -import android.support.test.rule.ActivityTestRule; +import android.support.test.espresso.intent.rule.IntentsTestRule; import android.support.test.runner.AndroidJUnit4; -import android.widget.ListView; -import com.robotium.solo.Solo; +import android.view.View; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.PreferenceActivity; @@ -23,6 +21,7 @@ import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.test.antennapod.EspressoTestUtils; +import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -34,20 +33,20 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import static android.support.test.InstrumentationRegistry.getInstrumentation; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.action.ViewActions.longClick; +import static android.support.test.espresso.action.ViewActions.scrollTo; import static android.support.test.espresso.intent.Intents.intended; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; import static android.support.test.espresso.matcher.ViewMatchers.isRoot; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; -import static de.test.antennapod.NthMatcher.first; import static de.test.antennapod.EspressoTestUtils.waitForView; +import static de.test.antennapod.NthMatcher.first; import static junit.framework.TestCase.assertTrue; +import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; /** * User interface tests for MainActivity drawer @@ -55,49 +54,24 @@ import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) public class NavigationDrawerTest { - private Solo solo; private UITestUtils uiTestUtils; - private SharedPreferences prefs; @Rule - public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); + public IntentsTestRule mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false); @Before public void setUp() throws IOException { - // override first launch preference - // do this BEFORE calling getActivity()! - EspressoTestUtils.clearAppData(); - prefs = InstrumentationRegistry.getContext() - .getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE); - prefs.edit().putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false).commit(); - - mActivityRule.launchActivity(new Intent()); - - Intents.init(); - Context context = mActivityRule.getActivity(); - uiTestUtils = new UITestUtils(context); + uiTestUtils = new UITestUtils(InstrumentationRegistry.getTargetContext()); uiTestUtils.setup(); - // create new database - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - - RatingDialog.init(context); - RatingDialog.saveRated(); - - solo = new Solo(getInstrumentation(), mActivityRule.getActivity()); + EspressoTestUtils.clearPreferences(); + EspressoTestUtils.makeNotFirstRun(); + EspressoTestUtils.clearDatabase(); } @After public void tearDown() throws Exception { uiTestUtils.tearDown(); - solo.finishOpenedActivities(); - Intents.release(); - PodDBAdapter.deleteDatabase(); - prefs.edit().clear().commit(); } private void openNavDrawer() { @@ -105,69 +79,66 @@ public class NavigationDrawerTest { onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); } + private ViewInteraction onDrawerItem(Matcher viewMatcher) { + return onView(allOf(viewMatcher, withId(R.id.txtvTitle))); + } + @Test - @FlakyTest public void testClickNavDrawer() throws Exception { uiTestUtils.addLocalFeedData(false); - - setHiddenDrawerItems(new ArrayList<>()); + UserPreferences.setHiddenDrawerItems(new ArrayList<>()); + mActivityRule.launchActivity(new Intent()); + MainActivity activity = mActivityRule.getActivity(); // queue openNavDrawer(); - solo.clickOnText(solo.getString(R.string.queue_label)); - solo.waitForView(R.id.recyclerView); - assertEquals(solo.getString(R.string.queue_label), getActionbarTitle()); + onDrawerItem(withText(R.string.queue_label)).perform(click()); + onView(isRoot()).perform(waitForView(withId(R.id.recyclerView), 1000)); + assertEquals(activity.getString(R.string.queue_label), activity.getSupportActionBar().getTitle()); // episodes openNavDrawer(); - solo.clickOnText(solo.getString(R.string.episodes_label)); - solo.waitForView(android.R.id.list); - assertEquals(solo.getString(R.string.episodes_label), getActionbarTitle()); + onDrawerItem(withText(R.string.episodes_label)).perform(click()); + onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000)); + assertEquals(activity.getString(R.string.episodes_label), activity.getSupportActionBar().getTitle()); // Subscriptions openNavDrawer(); - solo.clickOnText(solo.getString(R.string.subscriptions_label)); - solo.waitForView(R.id.subscriptions_grid); - assertEquals(solo.getString(R.string.subscriptions_label), getActionbarTitle()); + onDrawerItem(withText(R.string.subscriptions_label)).perform(click()); + onView(isRoot()).perform(waitForView(withId(R.id.subscriptions_grid), 1000)); + assertEquals(activity.getString(R.string.subscriptions_label), activity.getSupportActionBar().getTitle()); // downloads openNavDrawer(); - solo.clickOnText(solo.getString(R.string.downloads_label)); - solo.waitForView(android.R.id.list); - assertEquals(solo.getString(R.string.downloads_label), getActionbarTitle()); + onDrawerItem(withText(R.string.downloads_label)).perform(click()); + onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000)); + assertEquals(activity.getString(R.string.downloads_label), activity.getSupportActionBar().getTitle()); // playback history openNavDrawer(); - solo.clickOnText(solo.getString(R.string.playback_history_label)); - solo.waitForView(android.R.id.list); - assertEquals(solo.getString(R.string.playback_history_label), getActionbarTitle()); + onDrawerItem(withText(R.string.playback_history_label)).perform(click()); + onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000)); + assertEquals(activity.getString(R.string.playback_history_label), activity.getSupportActionBar().getTitle()); // add podcast openNavDrawer(); - solo.clickOnText(solo.getString(R.string.add_feed_label)); - solo.waitForView(R.id.txtvFeedurl); - assertEquals(solo.getString(R.string.add_feed_label), getActionbarTitle()); + onDrawerItem(withText(R.string.add_feed_label)).perform(click()); + onView(isRoot()).perform(waitForView(withId(R.id.txtvFeedurl), 1000)); + assertEquals(activity.getString(R.string.add_feed_label), activity.getSupportActionBar().getTitle()); // podcasts - ListView list = (ListView) solo.getView(R.id.nav_list); for (int i = 0; i < uiTestUtils.hostedFeeds.size(); i++) { Feed f = uiTestUtils.hostedFeeds.get(i); openNavDrawer(); - solo.scrollListToLine(list, i); - solo.clickOnText(f.getTitle()); - solo.waitForView(android.R.id.list); - assertEquals("", getActionbarTitle()); + onDrawerItem(withText(f.getTitle())).perform(scrollTo(), click()); + onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000)); + assertEquals("", activity.getSupportActionBar().getTitle()); } } - private String getActionbarTitle() { - return ((MainActivity) solo.getCurrentActivity()).getSupportActionBar().getTitle().toString(); - } - - @Test - @FlakyTest public void testGoToPreferences() { + mActivityRule.launchActivity(new Intent()); openNavDrawer(); onView(withText(R.string.settings_label)).perform(click()); intended(hasComponent(PreferenceActivity.class.getName())); @@ -175,9 +146,10 @@ public class NavigationDrawerTest { @Test public void testDrawerPreferencesHideSomeElements() { - setHiddenDrawerItems(new ArrayList<>()); + UserPreferences.setHiddenDrawerItems(new ArrayList<>()); + mActivityRule.launchActivity(new Intent()); openNavDrawer(); - onView(first(withText(R.string.queue_label))).perform(longClick()); + onDrawerItem(withText(R.string.queue_label)).perform(longClick()); onView(withText(R.string.episodes_label)).perform(click()); onView(withText(R.string.playback_history_label)).perform(click()); onView(withText(R.string.confirm_label)).perform(click()); @@ -191,7 +163,8 @@ public class NavigationDrawerTest { @Test public void testDrawerPreferencesUnhideSomeElements() { List hidden = Arrays.asList(PlaybackHistoryFragment.TAG, DownloadsFragment.TAG); - setHiddenDrawerItems(hidden); + UserPreferences.setHiddenDrawerItems(hidden); + mActivityRule.launchActivity(new Intent()); openNavDrawer(); onView(first(withText(R.string.queue_label))).perform(longClick()); @@ -208,7 +181,8 @@ public class NavigationDrawerTest { @Test public void testDrawerPreferencesHideAllElements() { - setHiddenDrawerItems(new ArrayList<>()); + UserPreferences.setHiddenDrawerItems(new ArrayList<>()); + mActivityRule.launchActivity(new Intent()); String[] titles = mActivityRule.getActivity().getResources().getStringArray(R.array.nav_drawer_titles); openNavDrawer(); @@ -227,7 +201,8 @@ public class NavigationDrawerTest { @Test public void testDrawerPreferencesHideCurrentElement() { - setHiddenDrawerItems(new ArrayList<>()); + UserPreferences.setHiddenDrawerItems(new ArrayList<>()); + mActivityRule.launchActivity(new Intent()); openNavDrawer(); onView(withText(R.string.downloads_label)).perform(click()); openNavDrawer(); @@ -240,14 +215,4 @@ public class NavigationDrawerTest { assertEquals(1, hidden.size()); assertTrue(hidden.contains(DownloadsFragment.TAG)); } - - private void setHiddenDrawerItems(List items) { - UserPreferences.setHiddenDrawerItems(items); - try { - mActivityRule.runOnUiThread(() -> mActivityRule.getActivity().updateNavDrawer()); - } catch (Throwable throwable) { - throwable.printStackTrace(); - fail(); - } - } } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackSonicTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackSonicTest.java index 6b41ad0ea..22959917d 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackSonicTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackSonicTest.java @@ -52,12 +52,11 @@ public class PlaybackSonicTest { @Before public void setUp() throws Exception { - EspressoTestUtils.clearAppData(); + EspressoTestUtils.clearPreferences(); + EspressoTestUtils.makeNotFirstRun(); + EspressoTestUtils.clearDatabase(); context = InstrumentationRegistry.getTargetContext(); - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.edit() .clear() @@ -71,11 +70,6 @@ public class PlaybackSonicTest { uiTestUtils = new UITestUtils(context); uiTestUtils.setup(); - - // create database - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); } @After @@ -122,7 +116,7 @@ public class PlaybackSonicTest { solo.clickOnText(solo.getString(R.string.all_episodes_short_label)); getInstrumentation().waitForIdleSync(); - final List episodes = DBReader.getRecentlyPublishedEpisodes(10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); assertTrue(solo.waitForView(solo.getView(R.id.butSecondaryAction))); solo.clickOnView(solo.getView(R.id.butSecondaryAction)); @@ -241,7 +235,7 @@ public class PlaybackSonicTest { setContinuousPlaybackPreference(followQueue); uiTestUtils.addLocalFeedData(true); DBWriter.clearQueue().get(); - final List episodes = DBReader.getRecentlyPublishedEpisodes(10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); startLocalPlayback(); long mediaId = episodes.get(0).getMedia().getId(); diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java index 099549a69..b302bcc91 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java @@ -114,7 +114,7 @@ public class PlaybackTest { solo.waitForText(solo.getString(R.string.all_episodes_short_label)); solo.clickOnText(solo.getString(R.string.all_episodes_short_label)); - final List episodes = DBReader.getRecentlyPublishedEpisodes(10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); assertTrue(solo.waitForView(solo.getView(R.id.butSecondaryAction))); solo.clickOnView(solo.getView(R.id.butSecondaryAction)); @@ -231,7 +231,7 @@ public class PlaybackTest { setContinuousPlaybackPreference(followQueue); uiTestUtils.addLocalFeedData(true); DBWriter.clearQueue().get(); - final List episodes = DBReader.getRecentlyPublishedEpisodes(10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); startLocalPlayback(); long mediaId = episodes.get(0).getMedia().getId(); 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 4b2dc75a9..1063ac90a 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -49,7 +49,7 @@ public class PreferencesTest { @Before public void setUp() { - EspressoTestUtils.clearAppData(); + EspressoTestUtils.clearPreferences(); mActivityRule.launchActivity(new Intent()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivityRule.getActivity()); prefs.edit().putBoolean(UserPreferences.PREF_ENABLE_AUTODL, true).commit(); diff --git a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java new file mode 100644 index 000000000..d1023dd9e --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java @@ -0,0 +1,58 @@ +package de.test.antennapod.ui; + +import android.content.Intent; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.intent.rule.IntentsTestRule; +import android.support.test.runner.AndroidJUnit4; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.fragment.QueueFragment; +import de.test.antennapod.EspressoTestUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +/** + * User interface tests for queue fragment + */ +@RunWith(AndroidJUnit4.class) +public class QueueFragmentTest { + + @Rule + public IntentsTestRule mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false); + + @Before + public void setUp() { + EspressoTestUtils.clearPreferences(); + EspressoTestUtils.makeNotFirstRun(); + EspressoTestUtils.clearDatabase(); + EspressoTestUtils.setLastNavFragment(QueueFragment.TAG); + mActivityRule.launchActivity(new Intent()); + } + + @Test + public void testLockEmptyQueue() { + onView(withContentDescription(R.string.lock_queue)).perform(click()); + onView(withContentDescription(R.string.unlock_queue)).perform(click()); + } + + @Test + public void testSortEmptyQueue() { + Espresso.openContextualActionModeOverflowMenu(); + onView(withText(R.string.sort)).perform(click()); + onView(withText(R.string.random)).perform(click()); + } + + @Test + public void testKeepEmptyQueueSorted() { + Espresso.openContextualActionModeOverflowMenu(); + onView(withText(R.string.sort)).perform(click()); + onView(withText(R.string.keep_sorted)).perform(click()); + } +} diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java index 20c54cb15..905c65c34 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java @@ -59,7 +59,7 @@ class UITestUtils { public void setup() throws IOException { destDir = new File(context.getFilesDir(), "test/UITestUtils"); - destDir.mkdir(); + destDir.mkdirs(); hostedFeedDir = new File(destDir, "hostedFeeds"); hostedFeedDir.mkdir(); hostedMediaDir = new File(destDir, "hostedMediaDir"); diff --git a/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java index 3d8417bf6..4158fd31c 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java +++ b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java @@ -72,20 +72,6 @@ public class HTTPBin extends NanoHTTPD { return servedFiles.size() - 1; } - /** - * Removes the file with the given ID from the server. - * - * @return True if a file was removed, false otherwise - */ - public synchronized boolean removeFile(int id) { - if (id < 0) throw new IllegalArgumentException("ID < 0"); - if (id >= servedFiles.size()) { - return false; - } else { - return servedFiles.remove(id) != null; - } - } - public synchronized File accessFile(int id) { if (id < 0 || id >= servedFiles.size()) { return null; diff --git a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java index 79d7e02f2..519d4c61b 100644 --- a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java +++ b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java @@ -7,212 +7,9 @@ import android.support.v7.app.AppCompatActivity; * network. */ public abstract class CastEnabledActivity extends AppCompatActivity { -// implements SharedPreferences.OnSharedPreferenceChangeListener { public static final String TAG = "CastEnabledActivity"; -// protected CastManager castManager; -// protected SwitchableMediaRouteActionProvider mediaRouteActionProvider; -// private final CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager(); -// -// @Override -// protected void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// -// PreferenceManager.getDefaultSharedPreferences(getApplicationContext()). -// registerOnSharedPreferenceChangeListener(this); -// -// castManager = CastManager.getInstance(); -// castManager.addCastConsumer(castConsumer); -// castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled()); -// onCastConnectionChanged(castManager.isConnected()); -// } -// -// @Override -// protected void onDestroy() { -// PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) -// .unregisterOnSharedPreferenceChangeListener(this); -// castManager.removeCastConsumer(castConsumer); -// super.onDestroy(); -// } -// -// @Override -// @CallSuper -// public boolean onCreateOptionsMenu(Menu menu) { -// super.onCreateOptionsMenu(menu); -// getMenuInflater().inflate(R.menu.cast_enabled, menu); -// castButtonVisibilityManager.setMenu(menu); -// return true; -// } -// -// @Override -// @CallSuper -// public boolean onPrepareOptionsMenu(Menu menu) { -// super.onPrepareOptionsMenu(menu); -// mediaRouteActionProvider = castManager -// .addMediaRouterButton(menu.findItem(R.id.media_route_menu_item)); -// mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable()); -// return true; -// } -// -// @Override -// protected void onResume() { -// super.onResume(); -// castButtonVisibilityManager.setResumed(true); -// } -// -// @Override -// protected void onPause() { -// super.onPause(); -// castButtonVisibilityManager.setResumed(false); -// } -// -// @Override -// public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { -// if (UserPreferences.PREF_CAST_ENABLED.equals(key)) { -// boolean newValue = UserPreferences.isCastEnabled(); -// Log.d(TAG, "onSharedPreferenceChanged(), isCastEnabled set to " + newValue); -// castButtonVisibilityManager.setPrefEnabled(newValue); -// // PlaybackService has its own listener, so if it's active we don't have to take action here. -// if (!newValue && !PlaybackService.isRunning) { -// CastManager.getInstance().disconnect(); -// } -// } -// } -// -// CastConsumer castConsumer = new DefaultCastConsumer() { -// @Override -// public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) { -// onCastConnectionChanged(true); -// } -// -// @Override -// public void onDisconnected() { -// onCastConnectionChanged(false); -// } -// }; -// -// private void onCastConnectionChanged(boolean connected) { -// if (connected) { -// castButtonVisibilityManager.onConnected(); -// setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); -// } else { -// castButtonVisibilityManager.onDisconnected(); -// setVolumeControlStream(AudioManager.STREAM_MUSIC); -// } -// } -// -// /** -// * Should be called by any activity or fragment for which the cast button should be shown. -// * -// * @param showAsAction refer to {@link MenuItem#setShowAsAction(int)} -// */ public final void requestCastButton(int showAsAction) { // no-op } - -// private class CastButtonVisibilityManager { -// private volatile boolean prefEnabled = false; -// private volatile boolean viewRequested = false; -// private volatile boolean resumed = false; -// private volatile boolean connected = false; -// private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; -// private Menu menu; -// -// public synchronized void setPrefEnabled(boolean newValue) { -// if (prefEnabled != newValue && resumed && (viewRequested || connected)) { -// if (newValue) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// prefEnabled = newValue; -// if (mediaRouteActionProvider != null) { -// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); -// } -// } -// -// public synchronized void setResumed(boolean newValue) { -// if (resumed == newValue) { -// Log.e(TAG, "resumed should never change to the same value"); -// return; -// } -// resumed = newValue; -// if (prefEnabled && (viewRequested || connected)) { -// if (resumed) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// } -// -// public synchronized void setViewRequested(boolean newValue) { -// if (viewRequested != newValue && resumed && prefEnabled && !connected) { -// if (newValue) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// viewRequested = newValue; -// if (mediaRouteActionProvider != null) { -// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); -// } -// } -// -// public synchronized void setConnected(boolean newValue) { -// if (connected != newValue && resumed && prefEnabled && !prefEnabled) { -// if (newValue) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// connected = newValue; -// if (mediaRouteActionProvider != null) { -// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); -// } -// } -// -// public synchronized boolean shouldEnable() { -// return prefEnabled && viewRequested; -// } -// -// public void setMenu(Menu menu) { -// setViewRequested(false); -// showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; -// this.menu = menu; -// setShowAsAction(); -// } -// -// public void requestCastButton(int showAsAction) { -// setViewRequested(true); -// this.showAsAction = showAsAction; -// setShowAsAction(); -// } -// -// public void onConnected() { -// setConnected(true); -// setShowAsAction(); -// } -// -// public void onDisconnected() { -// setConnected(false); -// setShowAsAction(); -// } -// -// private void setShowAsAction() { -// if (menu == null) { -// Log.d(TAG, "setShowAsAction() without a menu"); -// return; -// } -// MenuItem item = menu.findItem(R.id.media_route_menu_item); -// if (item == null) { -// Log.e(TAG, "setShowAsAction(), but cast button not inflated"); -// return; -// } -// MenuItemCompat.setShowAsAction(item, connected? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction); -// } -// } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 73af654e9..c2c6f53c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,7 +36,7 @@ android:usesCleartextTraffic="true" android:logo="@mipmap/ic_launcher"> + android:resource="@drawable/ic_antenna" /> @@ -209,6 +209,13 @@ android:name=".activity.OpmlFeedChooserActivity" android:label="@string/opml_import_label"> + + + { + IntentUtils.openInBrowser(BugReportActivity.this, "https://github.com/AntennaPod/AntennaPod/issues"); + }); + + findViewById(R.id.btn_copy_log).setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.bug_report_title), crashDetailsText.getText()); + clipboard.setPrimaryClip(clip); + Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show(); + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java index 871e9c279..c60c7b769 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java @@ -49,6 +49,7 @@ public class CastplayerActivity extends MediaplayerInfoActivity { super.setupGUI(); if (butPlaybackSpeed != null) { butPlaybackSpeed.setVisibility(View.GONE); + txtvPlaybackSpeed.setVisibility(View.GONE); } // if (butCastDisconnect != null) { // butCastDisconnect.setOnClickListener(v -> castManager.disconnect()); 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 102d30bcf..b028f7983 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.activity; import android.annotation.TargetApi; import android.app.ProgressDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -10,6 +11,7 @@ import android.database.DataSetObserver; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; @@ -32,9 +34,11 @@ import android.widget.Toast; import com.bumptech.glide.Glide; -import de.danoeh.antennapod.preferences.PreferenceUpgrader; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import java.util.List; @@ -43,7 +47,6 @@ import de.danoeh.antennapod.adapter.NavListAdapter; import de.danoeh.antennapod.core.asynctask.FeedRemover; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.MessageEvent; -import de.danoeh.antennapod.core.event.ProgressEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; @@ -56,25 +59,22 @@ import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.Flavors; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.dialog.RenameFeedDialog; import de.danoeh.antennapod.fragment.AddFeedFragment; import de.danoeh.antennapod.fragment.DownloadsFragment; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.ExternalPlayerFragment; -import de.danoeh.antennapod.fragment.ItemlistFragment; +import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.preferences.PreferenceUpgrader; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; /** * The activity that is shown when the user launches the app. @@ -88,13 +88,13 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi public static final String PREF_NAME = "MainActivityPrefs"; public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch"; - private static final String PREF_LAST_FRAGMENT_TAG = "prefMainActivityLastFragmentTag"; + public static final String PREF_LAST_FRAGMENT_TAG = "prefMainActivityLastFragmentTag"; public static final String EXTRA_NAV_TYPE = "nav_type"; public static final String EXTRA_NAV_INDEX = "nav_index"; 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"; + private static final String EXTRA_FEED_ID = "fragment_feed_id"; private static final String SAVE_BACKSTACK_COUNT = "backstackCount"; private static final String SAVE_TITLE = "title"; @@ -128,6 +128,14 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi private long lastBackButtonPressTime = 0; + @NonNull + public static Intent getIntentToOpenFeed(@NonNull Context context, long feedId) { + Intent intent = new Intent(context.getApplicationContext(), MainActivity.class); + intent.putExtra(MainActivity.EXTRA_FEED_ID, feedId); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + return intent; + } + @Override public void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getNoTitleTheme()); @@ -233,6 +241,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi private void checkFirstLaunch() { SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { + loadFragment(AddFeedFragment.TAG, null); new Handler().postDelayed(() -> drawerLayout.openDrawer(navDrawer), 1500); // for backward compatibility, we only change defaults for fresh installs @@ -240,7 +249,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi SharedPreferences.Editor edit = prefs.edit(); edit.putBoolean(PREF_IS_FIRST_LAUNCH, false); - edit.commit(); + edit.apply(); } } @@ -337,7 +346,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi } public void loadFeedFragmentById(long feedId, Bundle args) { - Fragment fragment = ItemlistFragment.newInstance(feedId); + Fragment fragment = FeedItemlistFragment.newInstance(feedId); if(args != null) { fragment.setArguments(args); } @@ -777,25 +786,6 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi loadData(); } - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ProgressEvent event) { - Log.d(TAG, "onEvent(" + event + ")"); - switch(event.action) { - case START: - pd = new ProgressDialog(this); - pd.setMessage(event.message); - pd.setIndeterminate(true); - pd.setCancelable(false); - pd.show(); - break; - case END: - if(pd != null) { - pd.dismiss(); - } - break; - } - } - @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(MessageEvent event) { Log.d(TAG, "onEvent(" + 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 3946400a4..b1269b833 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -22,8 +22,6 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.Button; -import android.widget.CheckBox; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; @@ -35,16 +33,14 @@ import com.bumptech.glide.Glide; import com.joanzapata.iconify.IconDrawable; import com.joanzapata.iconify.fonts.FontAwesomeIcons; -import java.util.Locale; - 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; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.Consumer; import de.danoeh.antennapod.core.util.Converter; @@ -62,12 +58,15 @@ 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.SleepTimerDialog; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** @@ -79,9 +78,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements private static final String PREFS = "MediaPlayerActivityPreferences"; private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; private static final int REQUEST_CODE_STORAGE = 42; - private static final float PLAYBACK_SPEED_STEP = 0.05f; - private static final float DEFAULT_MIN_PLAYBACK_SPEED = 0.5f; - private static final float DEFAULT_MAX_PLAYBACK_SPEED = 2.5f; PlaybackController controller; @@ -202,6 +198,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements }; } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + onPositionObserverUpdate(); + } + private static TextView getTxtvFFFromActivity(MediaplayerActivity activity) { return activity.txtvFF; } @@ -282,6 +283,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements controller.init(); loadMediaInfo(); onPositionObserverUpdate(); + EventBus.getDefault().register(this); } @Override @@ -294,6 +296,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if (disposable != null) { disposable.dispose(); } + EventBus.getDefault().unregister(this); super.onStop(); } @@ -330,6 +333,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements Playable media = controller.getMedia(); boolean isFeedMedia = media != null && (media instanceof FeedMedia); + menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia); // FeedMedia implies it belongs to a Feed + boolean hasWebsiteLink = ( getWebsiteLinkWithFallback(media) != null ); menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink); @@ -365,7 +370,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements menu.findItem(R.id.audio_controls).setIcon(new IconDrawable(this, FontAwesomeIcons.fa_sliders).color(textColor).actionBarSize()); } else { - menu.findItem(R.id.audio_controls).setVisible(false); + menu.findItem(R.id.audio_controls).setIcon(new IconDrawable(this, + FontAwesomeIcons.fa_sliders).color(0xffffffff).actionBarSize()); } return true; @@ -396,29 +402,24 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements return true; } else { if (media != null) { + final @Nullable FeedItem feedItem = getFeedItem(media); // some options option requires FeedItem switch (item.getItemId()) { case R.id.add_to_favorites_item: - if(media instanceof FeedMedia) { - FeedItem feedItem = ((FeedMedia)media).getItem(); - if(feedItem != null) { - DBWriter.addFavoriteItem(feedItem); - isFavorite = true; - invalidateOptionsMenu(); - Toast.makeText(this, R.string.added_to_favorites, Toast.LENGTH_SHORT) - .show(); - } + if (feedItem != null) { + DBWriter.addFavoriteItem(feedItem); + isFavorite = true; + invalidateOptionsMenu(); + Toast.makeText(this, R.string.added_to_favorites, Toast.LENGTH_SHORT) + .show(); } break; case R.id.remove_from_favorites_item: - if(media instanceof FeedMedia) { - FeedItem feedItem = ((FeedMedia)media).getItem(); - if(feedItem != null) { - DBWriter.removeFavoriteItem(feedItem); - isFavorite = false; - invalidateOptionsMenu(); - Toast.makeText(this, R.string.removed_from_favorites, Toast.LENGTH_SHORT) - .show(); - } + if (feedItem != null) { + DBWriter.removeFavoriteItem(feedItem); + isFavorite = false; + invalidateOptionsMenu(); + Toast.makeText(this, R.string.removed_from_favorites, Toast.LENGTH_SHORT) + .show(); } break; case R.id.disable_sleeptimer_item: @@ -451,171 +452,37 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } break; case R.id.audio_controls: - MaterialDialog dialog = new MaterialDialog.Builder(this) - .title(R.string.audio_controls) - .customView(R.layout.audio_controls, true) - .neutralText(R.string.close_label) - .onNeutral((dialog1, which) -> { - final SeekBar left = (SeekBar) dialog1.findViewById(R.id.volume_left); - final SeekBar right = (SeekBar) dialog1.findViewById(R.id.volume_right); - UserPreferences.setVolume(left.getProgress(), right.getProgress()); - }) - .show(); - final SeekBar barPlaybackSpeed = (SeekBar) dialog.findViewById(R.id.playback_speed); - final Button butDecSpeed = (Button) dialog.findViewById(R.id.butDecSpeed); - butDecSpeed.setOnClickListener(v -> { - if(controller != null && controller.canSetPlaybackSpeed()) { - barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() - 1); - } else { - VariableSpeedDialog.showGetPluginDialog(this); - } - }); - final Button butIncSpeed = (Button) dialog.findViewById(R.id.butIncSpeed); - butIncSpeed.setOnClickListener(v -> { - if(controller != null && controller.canSetPlaybackSpeed()) { - barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() + 1); - } else { - VariableSpeedDialog.showGetPluginDialog(this); - } - }); - - final TextView txtvPlaybackSpeed = (TextView) dialog.findViewById(R.id.txtvPlaybackSpeed); - float currentSpeed = 1.0f; - try { - currentSpeed = Float.parseFloat(UserPreferences.getPlaybackSpeed()); - } catch (NumberFormatException e) { - Log.e(TAG, Log.getStackTraceString(e)); - UserPreferences.setPlaybackSpeed(String.valueOf(currentSpeed)); + boolean isPlayingVideo = controller.getMedia().getMediaType() == MediaType.VIDEO; + PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(isPlayingVideo); + dialog.show(getSupportFragmentManager(), "playback_controls"); + break; + case R.id.open_feed_item: + if (feedItem != null) { + Intent intent = MainActivity.getIntentToOpenFeed(this, feedItem.getFeedId()); + startActivity(intent); } - - String[] availableSpeeds = UserPreferences.getPlaybackSpeedArray(); - final float minPlaybackSpeed = availableSpeeds.length > 1 ? - Float.valueOf(availableSpeeds[0]) : DEFAULT_MIN_PLAYBACK_SPEED; - float maxPlaybackSpeed = availableSpeeds.length > 1 ? - Float.valueOf(availableSpeeds[availableSpeeds.length - 1]) : DEFAULT_MAX_PLAYBACK_SPEED; - int progressMax = (int) ((maxPlaybackSpeed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP); - barPlaybackSpeed.setMax(progressMax); - - txtvPlaybackSpeed.setText(String.format("%.2fx", currentSpeed)); - barPlaybackSpeed.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if(controller != null && controller.canSetPlaybackSpeed()) { - float playbackSpeed = progress * PLAYBACK_SPEED_STEP + minPlaybackSpeed; - controller.setPlaybackSpeed(playbackSpeed); - String speedPref = String.format(Locale.US, "%.2f", playbackSpeed); - UserPreferences.setPlaybackSpeed(speedPref); - String speedStr = String.format("%.2fx", playbackSpeed); - txtvPlaybackSpeed.setText(speedStr); - } else if(fromUser) { - float speed = Float.valueOf(UserPreferences.getPlaybackSpeed()); - barPlaybackSpeed.post(() -> barPlaybackSpeed.setProgress( - (int) ((speed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP))); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - if(controller != null && !controller.canSetPlaybackSpeed()) { - VariableSpeedDialog.showGetPluginDialog(MediaplayerActivity.this); - } - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); - barPlaybackSpeed.setProgress((int) ((currentSpeed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP)); - - final SeekBar barLeftVolume = (SeekBar) dialog.findViewById(R.id.volume_left); - barLeftVolume.setProgress(UserPreferences.getLeftVolumePercentage()); - final SeekBar barRightVolume = (SeekBar) dialog.findViewById(R.id.volume_right); - barRightVolume.setProgress(UserPreferences.getRightVolumePercentage()); - final CheckBox stereoToMono = (CheckBox) dialog.findViewById(R.id.stereo_to_mono); - stereoToMono.setChecked(UserPreferences.stereoToMono()); - if (controller != null && !controller.canDownmix()) { - stereoToMono.setEnabled(false); - String sonicOnly = getString(R.string.sonic_only); - stereoToMono.setText(stereoToMono.getText() + " [" + sonicOnly + "]"); - } - - if (UserPreferences.useExoplayer()) { - barRightVolume.setEnabled(false); - } - - final CheckBox skipSilence = (CheckBox) dialog.findViewById(R.id.skipSilence); - skipSilence.setChecked(UserPreferences.isSkipSilence()); - if (!UserPreferences.useExoplayer()) { - skipSilence.setEnabled(false); - String exoplayerOnly = getString(R.string.exoplayer_only); - skipSilence.setText(skipSilence.getText() + " [" + exoplayerOnly + "]"); - } - skipSilence.setOnCheckedChangeListener((buttonView, isChecked) -> { - UserPreferences.setSkipSilence(isChecked); - controller.setSkipSilence(isChecked); - }); - - barLeftVolume.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - controller.setVolume( - Converter.getVolumeFromPercentage(progress), - Converter.getVolumeFromPercentage(barRightVolume.getProgress())); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); - barRightVolume.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - controller.setVolume( - Converter.getVolumeFromPercentage(barLeftVolume.getProgress()), - Converter.getVolumeFromPercentage(progress)); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); - stereoToMono.setOnCheckedChangeListener((buttonView, isChecked) -> { - UserPreferences.stereoToMono(isChecked); - if (controller != null) { - controller.setDownmix(isChecked); - } - }); break; case R.id.visit_website_item: - Uri uri = Uri.parse(getWebsiteLinkWithFallback(media)); - startActivity(new Intent(Intent.ACTION_VIEW, uri)); + IntentUtils.openInBrowser(MediaplayerActivity.this, getWebsiteLinkWithFallback(media)); break; case R.id.share_link_item: - if (media instanceof FeedMedia) { - ShareUtils.shareFeedItemLink(this, ((FeedMedia) media).getItem()); + if (feedItem != null) { + ShareUtils.shareFeedItemLink(this, feedItem); } break; case R.id.share_download_url_item: - if (media instanceof FeedMedia) { - ShareUtils.shareFeedItemDownloadLink(this, ((FeedMedia) media).getItem()); + if (feedItem != null) { + ShareUtils.shareFeedItemDownloadLink(this, feedItem); } break; case R.id.share_link_with_position_item: - if (media instanceof FeedMedia) { - ShareUtils.shareFeedItemLink(this, ((FeedMedia) media).getItem(), true); + if (feedItem != null) { + ShareUtils.shareFeedItemLink(this, feedItem, true); } break; case R.id.share_download_url_with_position_item: - if (media instanceof FeedMedia) { - ShareUtils.shareFeedItemDownloadLink(this, ((FeedMedia) media).getItem(), true); + if (feedItem != null) { + ShareUtils.shareFeedItemDownloadLink(this, feedItem, true); } break; case R.id.share_file: @@ -666,9 +533,10 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements return; } - int currentPosition = TimeSpeedConverter.convert(controller.getPosition()); - int duration = TimeSpeedConverter.convert(controller.getDuration()); - int remainingTime = TimeSpeedConverter.convert( + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int currentPosition = converter.convert(controller.getPosition()); + int duration = converter.convert(controller.getDuration()); + int remainingTime = converter.convert( controller.getDuration() - controller.getPosition()); Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); if (currentPosition == PlaybackService.INVALID_TIME || @@ -780,7 +648,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } } - static public void showSkipPreference(Activity activity, SkipDirection direction) { + public static void showSkipPreference(Activity activity, SkipDirection direction) { int checked = 0; int skipSecs = direction.getPrefSkipSeconds(); final int[] values = activity.getResources().getIntArray(R.array.seek_delta_values); @@ -824,14 +692,15 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements return; } + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); String length; if (showTimeLeft) { - int remainingTime = TimeSpeedConverter.convert( + int remainingTime = converter.convert( media.getDuration() - media.getPosition()); length = "-" + Converter.getDurationStringLong(remainingTime); } else { - int duration = TimeSpeedConverter.convert(media.getDuration()); + int duration = converter.convert(media.getDuration()); length = Converter.getDurationStringLong(duration); } txtvLength.setText(length); @@ -935,7 +804,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, txtvPosition); if (showTimeLeft && prog != 0) { int duration = controller.getDuration(); - int timeLeft = TimeSpeedConverter.convert(duration - (int) (prog * duration)); + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int timeLeft = converter.convert(duration - (int) (prog * duration)); String length = "-" + Converter.getDurationStringLong(timeLeft); txtvLength.setText(length); } @@ -956,11 +826,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } private void checkFavorite() { - Playable playable = controller.getMedia(); - if (!(playable instanceof FeedMedia)) { - return; - } - FeedItem feedItem = ((FeedMedia) playable).getItem(); + FeedItem feedItem = getFeedItem(controller.getMedia()); if (feedItem == null) { return; } @@ -1015,4 +881,13 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } } } + + @Nullable + private static FeedItem getFeedItem(@Nullable Playable playable) { + if ((playable != null) && (playable instanceof FeedMedia)) { + return ((FeedMedia)playable).getItem(); + } else { + return null; + } + } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java index 4fec1cfc5..234962a8d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java @@ -26,6 +26,7 @@ import android.widget.AdapterView; import android.widget.Button; import android.widget.ImageButton; import android.widget.ListView; +import android.widget.TextView; import android.widget.Toast; import com.viewpagerindicator.CirclePageIndicator; @@ -92,7 +93,8 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem NavListAdapter.SUBSCRIPTION_LIST_TAG }; - Button butPlaybackSpeed; + ImageButton butPlaybackSpeed; + TextView txtvPlaybackSpeed; ImageButton butCastDisconnect; private DrawerLayout drawerLayout; private NavListAdapter navAdapter; @@ -120,7 +122,6 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem disposable.dispose(); } EventDistributor.getInstance().unregister(contentUpdate); - EventBus.getDefault().unregister(this); saveCurrentFragment(); } @@ -173,7 +174,6 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem protected void onStart() { super.onStart(); EventDistributor.getInstance().register(contentUpdate); - EventBus.getDefault().register(this); loadData(); } @@ -258,6 +258,7 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem }); butPlaybackSpeed = findViewById(R.id.butPlaybackSpeed); + txtvPlaybackSpeed = findViewById(R.id.txtvPlaybackSpeed); butCastDisconnect = findViewById(R.id.butCastDisconnect); pager = findViewById(R.id.pager); 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 a0c449275..732570d8a 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -32,8 +32,6 @@ import android.widget.TextView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.core.feed.VolumeReductionSetting; -import de.danoeh.antennapod.core.glide.FastBlurTransformation; import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -55,7 +53,9 @@ import de.danoeh.antennapod.core.feed.EventDistributor; 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.VolumeReductionSetting; import de.danoeh.antennapod.core.glide.ApGlideSettings; +import de.danoeh.antennapod.core.glide.FastBlurTransformation; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; @@ -443,11 +443,9 @@ public class OnlineFeedViewActivity extends AppCompatActivity { subscribeButton.setOnClickListener(v -> { if(feedInFeedlist(feed)) { - Intent intent = new Intent(OnlineFeedViewActivity.this, MainActivity.class); // feed.getId() is always 0, we have to retrieve the id from the feed list from // the database - intent.putExtra(MainActivity.EXTRA_FEED_ID, getFeedId(feed)); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + Intent intent = MainActivity.getIntentToOpenFeed(this, getFeedId(feed)); startActivity(intent); } else { Feed f = new Feed(selectedDownloadUrl, null, feed.getTitle()); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java index 2eff33339..9eb181edc 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.adapter; import android.os.Build; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; @@ -23,10 +24,12 @@ import android.widget.TextView; import com.joanzapata.iconify.Iconify; import java.lang.ref.WeakReference; +import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; +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.storage.DownloadRequester; @@ -50,6 +53,7 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter payload) { + onBindViewHolder(holder, pos); + + if (holder == currentlyPlayingItem && payload.size() == 1 && payload.get(0) instanceof PlaybackPositionEvent) { + PlaybackPositionEvent event = (PlaybackPositionEvent) payload.get(0); + holder.progress.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); + } + } + + public void notifyCurrentlyPlayingItemChanged(PlaybackPositionEvent event) { + if (currentlyPlayingItem != null && currentlyPlayingItem.getAdapterPosition() != RecyclerView.NO_POSITION) { + notifyItemChanged(currentlyPlayingItem.getAdapterPosition(), event); + } + } + @Nullable public FeedItem getSelectedItem() { return selectedItem; @@ -262,7 +283,7 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter { .replaceAll("\\s+", " ") .trim(); holder.description.setText(description); + + final int MAX_LINES_COLLAPSED = 3; + holder.description.setMaxLines(MAX_LINES_COLLAPSED); + holder.description.setOnClickListener(v -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && holder.description.getMaxLines() > MAX_LINES_COLLAPSED) { + holder.description.setMaxLines(MAX_LINES_COLLAPSED); + } else { + holder.description.setMaxLines(2000); + } + }); } return convertView; } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java index 382abfb32..148b36d21 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.adapter; -import android.content.Context; import android.os.Build; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v4.view.MotionEventCompat; @@ -25,9 +25,11 @@ import android.widget.TextView; import com.joanzapata.iconify.Iconify; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import org.apache.commons.lang3.ArrayUtils; import java.lang.ref.WeakReference; +import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; @@ -58,6 +60,7 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter payload) { + onBindViewHolder(holder, pos); + + if (holder == currentlyPlayingItem && payload.size() == 1 && payload.get(0) instanceof PlaybackPositionEvent) { + PlaybackPositionEvent event = (PlaybackPositionEvent) payload.get(0); + holder.progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); + holder.progressLeft.setText(Converter.getDurationStringLong(event.getPosition())); + holder.progressRight.setText(Converter.getDurationStringLong(event.getDuration())); + } + } + @Nullable public FeedItem getSelectedItem() { return selectedItem; @@ -109,6 +124,12 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter exportObservable() { + DocumentFile output = DocumentFile.fromSingleUri(context, outputFileUri); + return Observable.create(subscriber -> { + OutputStream outputStream = null; + OutputStreamWriter writer = null; + try { + Uri uri = output.getUri(); + if (uri == null) { + throw new FileNotFoundException("Export file not found."); + } + outputStream = context.getContentResolver().openOutputStream(uri); + if (outputStream == null) { + throw new IOException(); + } + writer = new OutputStreamWriter(outputStream, LangUtils.UTF_8); + exportWriter.writeDocument(DBReader.getFeedList(), writer); + subscriber.onNext(output); + } catch (IOException e) { + subscriber.onError(e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + subscriber.onError(e); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + subscriber.onError(e); + } + } + subscriber.onComplete(); + } + }); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java index eb70d8e0b..c3f5d898c 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java @@ -35,6 +35,6 @@ public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks { @Override public int getNotificationIconResource(Context context) { - return R.drawable.ic_stat_antenna_default; + return R.drawable.ic_antenna; } } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java index c185a5557..4cfa7e870 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java @@ -9,7 +9,7 @@ import de.danoeh.antennapod.adapter.DataFolderAdapter; public class ChooseDataFolderDialog { - public static abstract class RunnableWithString implements Runnable { + public abstract static class RunnableWithString implements Runnable { public RunnableWithString() { super(); } 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 bb47f8baa..ed35495fa 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -3,7 +3,6 @@ package de.danoeh.antennapod.dialog; import android.app.AlertDialog; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.NonNull; @@ -13,7 +12,6 @@ import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.util.ArrayMap; -import android.support.v4.view.ViewCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.util.Log; @@ -168,7 +166,8 @@ public class EpisodesApplyActionFragment extends Fragment { return true; }); - for(FeedItem episode : episodes) { + titles.clear(); + for (FeedItem episode : episodes) { titles.add(episode.getTitle()); } @@ -205,10 +204,6 @@ public class EpisodesApplyActionFragment extends Fragment { return true; }); - if (Build.VERSION.SDK_INT == 23 || Build.VERSION.SDK_INT == 24) { - ViewCompat.setElevation(view.findViewById(R.id.fabSDScrollCtr), 8); - } - showSpeedDialIfAnyChecked(); return view; @@ -221,7 +216,13 @@ public class EpisodesApplyActionFragment extends Fragment { } private void showSpeedDialIfAnyChecked() { - mSpeedDialView.setVisibility(checkedIds.size() > 0 ? View.VISIBLE : View.GONE); + if (checkedIds.size() > 0) { + if (!mSpeedDialView.isShown()) { + mSpeedDialView.show(); + } + } else { + mSpeedDialView.hide(); // hide() also handles UI, e.g., overlay properly. + } } @Override @@ -245,10 +246,13 @@ public class EpisodesApplyActionFragment extends Fragment { // Prepare icon for select toggle button int[] icon = new int[1]; + @StringRes int titleResId; if (checkedIds.size() == episodes.size()) { icon[0] = R.attr.ic_select_none; + titleResId = R.string.deselect_all_label; } else { icon[0] = R.attr.ic_select_all; + titleResId = R.string.select_all_label; } TypedArray a = getActivity().obtainStyledAttributes(icon); @@ -256,6 +260,7 @@ public class EpisodesApplyActionFragment extends Fragment { a.recycle(); mSelectToggle.setIcon(iconDrawable); + mSelectToggle.setTitle(titleResId); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java new file mode 100644 index 000000000..607084c42 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java @@ -0,0 +1,62 @@ +package de.danoeh.antennapod.dialog; + +import android.content.Context; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItemFilter; + +public abstract class FilterDialog { + + protected FeedItemFilter filter; + protected Context context; + + public FilterDialog(Context context, FeedItemFilter feedItemFilter) { + this.context = context; + this.filter = feedItemFilter; + } + + public void openDialog() { + final String[] items = context.getResources().getStringArray(R.array.episode_filter_options); + final String[] values = context.getResources().getStringArray(R.array.episode_filter_values); + final boolean[] checkedItems = new boolean[items.length]; + + final Set filterValues = new HashSet<>(Arrays.asList(filter.getValues())); + + // make sure we have no empty strings in the filter list + for (String filterValue : filterValues) { + if (TextUtils.isEmpty(filterValue)) { + filterValues.remove(filterValue); + } + } + + for (int i = 0; i < values.length; i++) { + String value = values[i]; + if (filterValues.contains(value)) { + checkedItems[i] = true; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.filter); + builder.setMultiChoiceItems(items, checkedItems, (dialog, which, isChecked) -> { + if (isChecked) { + filterValues.add(values[which]); + } else { + filterValues.remove(values[which]); + } + }); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + updateFilter(filterValues); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + protected abstract void updateFilter(Set filterValues); +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java new file mode 100644 index 000000000..e8c7520b7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java @@ -0,0 +1,214 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.SeekBar; +import android.widget.TextView; +import com.afollestad.materialdialogs.MaterialDialog; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.PlaybackController; + +import java.util.Locale; + +public class PlaybackControlsDialog extends DialogFragment { + private static final float PLAYBACK_SPEED_STEP = 0.05f; + private static final float DEFAULT_MIN_PLAYBACK_SPEED = 0.5f; + private static final float DEFAULT_MAX_PLAYBACK_SPEED = 2.5f; + private static final String ARGUMENT_IS_PLAYING_VIDEO = "isPlayingVideo"; + + private PlaybackController controller; + private MaterialDialog dialog; + private boolean isPlayingVideo; + + public static PlaybackControlsDialog newInstance(boolean isPlayingVideo) { + Bundle arguments = new Bundle(); + arguments.putBoolean(ARGUMENT_IS_PLAYING_VIDEO, isPlayingVideo); + PlaybackControlsDialog dialog = new PlaybackControlsDialog(); + dialog.setArguments(arguments); + return dialog; + } + + public PlaybackControlsDialog() { + // Empty constructor required for DialogFragment + } + + @Override + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity(), false); + controller.init(); + setupUi(); + } + + @Override + public void onStop() { + super.onStop(); + controller.release(); + controller = null; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + isPlayingVideo = getArguments() != null && getArguments().getBoolean(ARGUMENT_IS_PLAYING_VIDEO); + + dialog = new MaterialDialog.Builder(getContext()) + .title(R.string.audio_controls) + .customView(R.layout.audio_controls, true) + .neutralText(R.string.close_label) + .onNeutral((dialog1, which) -> { + final SeekBar left = (SeekBar) dialog1.findViewById(R.id.volume_left); + final SeekBar right = (SeekBar) dialog1.findViewById(R.id.volume_right); + UserPreferences.setVolume(left.getProgress(), right.getProgress()); + }).build(); + return dialog; + } + + private void setupUi() { + final SeekBar barPlaybackSpeed = (SeekBar) dialog.findViewById(R.id.playback_speed); + final Button butDecSpeed = (Button) dialog.findViewById(R.id.butDecSpeed); + butDecSpeed.setOnClickListener(v -> { + if (controller != null && controller.canSetPlaybackSpeed()) { + barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() - 1); + } else { + VariableSpeedDialog.showGetPluginDialog(getContext()); + } + }); + final Button butIncSpeed = (Button) dialog.findViewById(R.id.butIncSpeed); + butIncSpeed.setOnClickListener(v -> { + if (controller != null && controller.canSetPlaybackSpeed()) { + barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() + 1); + } else { + VariableSpeedDialog.showGetPluginDialog(getContext()); + } + }); + + final TextView txtvPlaybackSpeed = (TextView) dialog.findViewById(R.id.txtvPlaybackSpeed); + float currentSpeed = getCurrentSpeed(); + + String[] availableSpeeds = UserPreferences.getPlaybackSpeedArray(); + final float minPlaybackSpeed = availableSpeeds.length > 1 ? + Float.valueOf(availableSpeeds[0]) : DEFAULT_MIN_PLAYBACK_SPEED; + float maxPlaybackSpeed = availableSpeeds.length > 1 ? + Float.valueOf(availableSpeeds[availableSpeeds.length - 1]) : DEFAULT_MAX_PLAYBACK_SPEED; + int progressMax = (int) ((maxPlaybackSpeed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP); + barPlaybackSpeed.setMax(progressMax); + + txtvPlaybackSpeed.setText(String.format("%.2fx", currentSpeed)); + barPlaybackSpeed.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (controller != null && controller.canSetPlaybackSpeed()) { + float playbackSpeed = progress * PLAYBACK_SPEED_STEP + minPlaybackSpeed; + controller.setPlaybackSpeed(playbackSpeed); + String speedPref = String.format(Locale.US, "%.2f", playbackSpeed); + + if (isPlayingVideo) { + UserPreferences.setVideoPlaybackSpeed(speedPref); + } else { + UserPreferences.setPlaybackSpeed(speedPref); + } + + String speedStr = String.format("%.2fx", playbackSpeed); + txtvPlaybackSpeed.setText(speedStr); + } else if (fromUser) { + float speed = getCurrentSpeed(); + barPlaybackSpeed.post(() -> barPlaybackSpeed.setProgress( + (int) ((speed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP))); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (controller != null && !controller.canSetPlaybackSpeed()) { + VariableSpeedDialog.showGetPluginDialog(getContext()); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + barPlaybackSpeed.setProgress((int) ((currentSpeed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP)); + + final SeekBar barLeftVolume = (SeekBar) dialog.findViewById(R.id.volume_left); + barLeftVolume.setProgress(UserPreferences.getLeftVolumePercentage()); + final SeekBar barRightVolume = (SeekBar) dialog.findViewById(R.id.volume_right); + barRightVolume.setProgress(UserPreferences.getRightVolumePercentage()); + final CheckBox stereoToMono = (CheckBox) dialog.findViewById(R.id.stereo_to_mono); + stereoToMono.setChecked(UserPreferences.stereoToMono()); + if (controller != null && !controller.canDownmix()) { + stereoToMono.setEnabled(false); + String sonicOnly = getString(R.string.sonic_only); + stereoToMono.setText(stereoToMono.getText() + " [" + sonicOnly + "]"); + } + + if (UserPreferences.useExoplayer()) { + barRightVolume.setEnabled(false); + } + + final CheckBox skipSilence = (CheckBox) dialog.findViewById(R.id.skipSilence); + skipSilence.setChecked(UserPreferences.isSkipSilence()); + if (!UserPreferences.useExoplayer()) { + skipSilence.setEnabled(false); + String exoplayerOnly = getString(R.string.exoplayer_only); + skipSilence.setText(skipSilence.getText() + " [" + exoplayerOnly + "]"); + } + skipSilence.setOnCheckedChangeListener((buttonView, isChecked) -> { + UserPreferences.setSkipSilence(isChecked); + controller.setSkipSilence(isChecked); + }); + + barLeftVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + controller.setVolume( + Converter.getVolumeFromPercentage(progress), + Converter.getVolumeFromPercentage(barRightVolume.getProgress())); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + barRightVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + controller.setVolume( + Converter.getVolumeFromPercentage(barLeftVolume.getProgress()), + Converter.getVolumeFromPercentage(progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + stereoToMono.setOnCheckedChangeListener((buttonView, isChecked) -> { + UserPreferences.stereoToMono(isChecked); + if (controller != null) { + controller.setDownmix(isChecked); + } + }); + } + + private float getCurrentSpeed() { + if (isPlayingVideo) { + return UserPreferences.getVideoPlaybackSpeed(); + } + return UserPreferences.getPlaybackSpeed(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java index 24656ed29..5969963f2 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java @@ -15,6 +15,7 @@ import java.lang.ref.WeakReference; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.IntentUtils; public class RatingDialog { @@ -59,14 +60,10 @@ public class RatingDialog { private static void rateNow() { Context context = mContext.get(); - if(context == null) { + if (context == null) { return; } - final String appPackage = "de.danoeh.antennapod"; - final Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=" + appPackage); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); + IntentUtils.openInBrowser(context, "https://play.google.com/store/apps/details?id=de.danoeh.antennapod"); saveRated(); } 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 cf9a2907b..bf3faf89a 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java @@ -32,6 +32,7 @@ public class VariableSpeedDialog { public static void showDialog(final Context context) { if (org.antennapod.audio.MediaPlayer.isPrestoLibraryInstalled(context) || UserPreferences.useSonic() + || UserPreferences.useExoplayer() || Build.VERSION.SDK_INT >= 23) { showSpeedSelectorDialog(context); } else { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java index 18d65a03c..e34d8539c 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java @@ -24,7 +24,7 @@ public class CombinedSearcher implements PodcastSearcher { public CombinedSearcher(Context context) { addProvider(new FyydPodcastSearcher(), 1.f); addProvider(new ItunesPodcastSearcher(context), 1.f); - addProvider(new GpodnetPodcastSearcher(), 0.6f); + //addProvider(new GpodnetPodcastSearcher(), 0.6f); } private void addProvider(PodcastSearcher provider, float priority) { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java index bc9133258..79ccd9532 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java @@ -33,16 +33,12 @@ public class ItunesTopListLoader { OkHttpClient client = AntennapodHttpClient.getHttpClient(); String feedString; try { - try { - feedString = getTopListFeed(client, lang, limit); - } catch (IOException e) { - feedString = getTopListFeed(client, "us", limit); - } - List podcasts = parseFeed(feedString); - emitter.onSuccess(podcasts); - } catch (IOException | JSONException e) { - emitter.onError(e); + feedString = getTopListFeed(client, lang, limit); + } catch (IOException e) { + feedString = getTopListFeed(client, "us", limit); } + List podcasts = parseFeed(feedString); + emitter.onSuccess(podcasts); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); 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 35bcaa76e..3ef010f88 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -70,11 +70,8 @@ public class AddFeedFragment extends Fragment { combinedFeedSearchBox = root.findViewById(R.id.combinedFeedSearchBox); combinedFeedSearchBox.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - performSearch(); - return true; - } - return false; + performSearch(); + return true; }); } 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 62d798cf6..bb52b26b7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -1,18 +1,9 @@ package de.danoeh.antennapod.fragment; -import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; import android.os.Bundle; -import android.os.Handler; import android.support.annotation.NonNull; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.SearchView; -import android.support.v7.widget.SimpleItemAnimator; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -20,231 +11,50 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ProgressBar; -import android.widget.Toast; - -import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.List; - +import com.joanzapata.iconify.Iconify; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; -import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.feed.EventDistributor; -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.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.view.EmptyViewHandler; +import de.danoeh.antennapod.dialog.FilterDialog; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import java.util.List; +import java.util.Set; + /** - * Shows unread or recently published episodes + * Like 'EpisodesFragment' except that it only shows new episodes and + * supports swiping to mark as read. */ -public class AllEpisodesFragment extends Fragment { +public class AllEpisodesFragment extends EpisodesListFragment { public static final String TAG = "AllEpisodesFragment"; + private static final String PREF_NAME = "PrefAllEpisodesFragment"; - private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE | - EventDistributor.UNREAD_ITEMS_UPDATE | - EventDistributor.PLAYER_STATUS_UPDATE; + private static final int EPISODES_PER_PAGE = 150; + private static final int VISIBLE_EPISODES_SCROLL_THRESHOLD = 5; + private static int page = 1; - private static final int RECENT_EPISODES_LIMIT = 150; - private static final String DEFAULT_PREF_NAME = "PrefAllEpisodesFragment"; - private static final String PREF_SCROLL_POSITION = "scroll_position"; - private static final String PREF_SCROLL_OFFSET = "scroll_offset"; + private static FeedItemFilter feedItemFilter = new FeedItemFilter(""); - RecyclerView recyclerView; - AllEpisodesRecycleAdapter listAdapter; - private ProgressBar progLoading; - EmptyViewHandler emptyView; - - @NonNull - List episodes = new ArrayList<>(); - @NonNull - private List downloaderList = new ArrayList<>(); - - private boolean isUpdatingFeeds; - boolean isMenuInvalidationAllowed = false; - - Disposable disposable; - private LinearLayoutManager layoutManager; - - boolean showOnlyNewEpisodes() { + @Override + protected boolean showOnlyNewEpisodes() { return false; } - String getPrefName() { - return DEFAULT_PREF_NAME; - } - @Override - public void onStart() { - super.onStart(); - setHasOptionsMenu(true); - EventDistributor.getInstance().register(contentUpdate); - EventBus.getDefault().register(this); - loadItems(); - } - - @Override - public void onResume() { - super.onResume(); - registerForContextMenu(recyclerView); - } - - @Override - public void onPause() { - super.onPause(); - saveScrollPosition(); - unregisterForContextMenu(recyclerView); - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - EventDistributor.getInstance().unregister(contentUpdate); - if (disposable != null) { - disposable.dispose(); - } - } - - private void saveScrollPosition() { - int firstItem = layoutManager.findFirstVisibleItemPosition(); - View firstItemView = layoutManager.findViewByPosition(firstItem); - float topOffset; - if (firstItemView == null) { - topOffset = 0; - } else { - topOffset = firstItemView.getTop(); - } - - SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(PREF_SCROLL_POSITION, firstItem); - editor.putFloat(PREF_SCROLL_OFFSET, topOffset); - editor.commit(); - } - - private void restoreScrollPosition() { - SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE); - int position = prefs.getInt(PREF_SCROLL_POSITION, 0); - float offset = prefs.getFloat(PREF_SCROLL_OFFSET, 0.0f); - if (position > 0 || offset > 0) { - layoutManager.scrollToPositionWithOffset(position, (int) offset); - // restore once, then forget - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(PREF_SCROLL_POSITION, 0); - editor.putFloat(PREF_SCROLL_OFFSET, 0.0f); - editor.commit(); - } - } - - private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = - () -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (!isAdded()) { - return; - } - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.episodes, menu); - - MenuItem searchItem = menu.findItem(R.id.action_search); - final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); - MenuItemUtils.adjustTextColor(getActivity(), sv); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) requireActivity()).loadChildFragment(SearchFragment.newInstance(s)); - return true; - } - - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - MenuItem markAllRead = menu.findItem(R.id.mark_all_read_item); - if (markAllRead != null) { - markAllRead.setVisible(!showOnlyNewEpisodes() && !episodes.isEmpty()); - } - MenuItem removeAllNewFlags = menu.findItem(R.id.remove_all_new_flags_item); - if (removeAllNewFlags != null) { - removeAllNewFlags.setVisible(showOnlyNewEpisodes() && !episodes.isEmpty()); - } + protected String getPrefName() { + return PREF_NAME; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (!super.onOptionsItemSelected(item)) { switch (item.getItemId()) { - case R.id.refresh_item: - List feeds = ((MainActivity) getActivity()).getFeeds(); - if (feeds != null) { - DBTasks.refreshAllFeeds(getActivity(), feeds); - } - return true; - case R.id.mark_all_read_item: - ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(), - R.string.mark_all_read_label, - R.string.mark_all_read_confirmation_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.markAllItemsRead(); - Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show(); - } - }; - markAllReadConfirmationDialog.createNewDialog().show(); - return true; - case R.id.remove_all_new_flags_item: - ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(), - R.string.remove_all_new_flags_label, - R.string.remove_all_new_flags_confirmation_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.removeAllNewFlags(); - Toast.makeText(getActivity(), R.string.removed_all_new_flags_msg, Toast.LENGTH_SHORT).show(); - } - }; - removeAllNewFlagsConfirmationDialog.createNewDialog().show(); + case R.id.filter_items: + showFilterDialog(); return true; default: return false; @@ -252,250 +62,105 @@ public class AllEpisodesFragment extends Fragment { } else { return true; } - - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); - if (!getUserVisibleHint()) { - return false; - } - if (!isVisible()) { - return false; - } - if (item.getItemId() == R.id.share_item) { - return true; // avoids that the position is reset when we need it in the submenu - } - - if (listAdapter.getSelectedItem() == null) { - Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); - return super.onContextItemSelected(item); - } - FeedItem selectedItem = listAdapter.getSelectedItem(); - - // Remove new flag contains UI logic specific to All/New/FavoriteSegments, - // e.g., Undo with Snackbar, - // and is handled by this class rather than the generic FeedItemMenuHandler - // Undo is useful for Remove new flag, given there is no UI to undo it otherwise, - // i.e., there is context menu item for Mark as new - if (R.id.remove_new_flag_item == item.getItemId()) { - removeNewFlagWithUndo(selectedItem); - return true; - } - - return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); } @NonNull @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View root = inflater.inflate(R.layout.all_episodes_fragment, container, false); + View root = super.onCreateView(inflater, container, savedInstanceState); - layoutManager = new LinearLayoutManager(getActivity()); - recyclerView = root.findViewById(android.R.id.list); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setHasFixedSize(true); - recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); - recyclerView.setVisibility(View.GONE); + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); - if (animator instanceof SimpleItemAnimator) { - ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); - } + /* Total number of episodes after last load */ + private int previousTotalEpisodes = 0; - progLoading = root.findViewById(R.id.progLoading); - progLoading.setVisibility(View.VISIBLE); + /* True if loading more episodes is still in progress */ + private boolean isLoadingMore = true; - emptyView = new EmptyViewHandler(getContext()); - emptyView.attachToRecyclerView(recyclerView); - emptyView.setIcon(R.attr.feed); - emptyView.setTitle(R.string.no_all_episodes_head_label); - emptyView.setMessage(R.string.no_all_episodes_label); + @Override + public void onScrolled(RecyclerView recyclerView, int deltaX, int deltaY) { + super.onScrolled(recyclerView, deltaX, deltaY); - createRecycleAdapter(recyclerView, emptyView); - emptyView.hide(); + int visibleEpisodeCount = recyclerView.getChildCount(); + int totalEpisodeCount = recyclerView.getLayoutManager().getItemCount(); + int firstVisibleEpisode = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + + /* Determine if loading more episodes has finished */ + if (isLoadingMore) { + if (totalEpisodeCount > previousTotalEpisodes) { + isLoadingMore = false; + previousTotalEpisodes = totalEpisodeCount; + } + } + + /* Determine if the user scrolled to the bottom and loading more episodes is not already in progress */ + if (!isLoadingMore && (totalEpisodeCount - visibleEpisodeCount) + <= (firstVisibleEpisode + VISIBLE_EPISODES_SCROLL_THRESHOLD)) { + + /* The end of the list has been reached. Load more data. */ + page++; + loadMoreItems(); + isLoadingMore = true; + } + } + }); return root; } - private void onFragmentLoaded(List episodes) { - this.episodes = episodes; - listAdapter.notifyDataSetChanged(); - - if (episodes.size() == 0) { - createRecycleAdapter(recyclerView, emptyView); - } - - restoreScrollPosition(); - requireActivity().invalidateOptionsMenu(); + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + menu.findItem(R.id.filter_items).setVisible(true); + menu.findItem(R.id.mark_all_read_item).setVisible(!episodes.isEmpty()); } - /** - * Currently, we need to recreate the list adapter in order to be able to undo last item via the - * snackbar. See #3084 for details. - */ - private void createRecycleAdapter(RecyclerView recyclerView, EmptyViewHandler emptyViewHandler) { - MainActivity mainActivity = (MainActivity) getActivity(); - listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, showOnlyNewEpisodes()); - listAdapter.setHasStableIds(true); - recyclerView.setAdapter(listAdapter); - emptyViewHandler.updateAdapter(listAdapter); - } + @Override + protected void onFragmentLoaded(List episodes) { + super.onFragmentLoaded(episodes); - private final AllEpisodesRecycleAdapter.ItemAccess itemAccess = new AllEpisodesRecycleAdapter.ItemAccess() { - - @Override - public int getCount() { - return episodes.size(); - } - - @Override - public FeedItem getItem(int position) { - if (0 <= position && position < episodes.size()) { - return episodes.get(position); - } - return null; - } - - @Override - public LongList getItemsIds() { - LongList ids = new LongList(episodes.size()); - for (FeedItem episode : episodes) { - ids.add(episode.getId()); - } - return ids; - } - - @Override - public int getItemDownloadProgressPercent(FeedItem item) { - for (Downloader downloader : downloaderList) { - DownloadRequest downloadRequest = downloader.getDownloadRequest(); - if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA - && downloadRequest.getFeedfileId() == item.getMedia().getId()) { - return downloadRequest.getProgressPercent(); - } - } - return 0; - } - - @Override - public boolean isInQueue(FeedItem item) { - return item != null && item.isTagged(FeedItem.TAG_QUEUE); - } - - @Override - public LongList getQueueIds() { - LongList queueIds = new LongList(); - for (FeedItem item : episodes) { - if (item.isTagged(FeedItem.TAG_QUEUE)) { - queueIds.add(item.getId()); - } - } - return queueIds; - } - - }; - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - for (FeedItem item : event.items) { - int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); - if (pos >= 0) { - episodes.remove(pos); - if (shouldUpdatedItemRemainInList(item)) { - episodes.add(pos, item); - listAdapter.notifyItemChanged(pos); - } else { - listAdapter.notifyItemRemoved(pos); - } - } + if (feedItemFilter.getValues().length > 0) { + txtvInformation.setText("{fa-info-circle} " + this.getString(R.string.filtered_label)); + Iconify.addIcons(txtvInformation); + txtvInformation.setVisibility(View.VISIBLE); + } else { + txtvInformation.setVisibility(View.GONE); } } - protected boolean shouldUpdatedItemRemainInList(FeedItem item) { - return true; - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - DownloaderUpdate update = event.update; - downloaderList = update.downloaders; - if (isMenuInvalidationAllowed && isUpdatingFeeds != update.feedIds.length > 0) { - requireActivity().invalidateOptionsMenu(); - } - if (update.mediaIds.length > 0) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId); - if (pos >= 0) { - listAdapter.notifyItemChanged(pos); - } - } - } - } - - private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((arg & EVENTS) != 0) { - loadItems(); - if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) { - requireActivity().invalidateOptionsMenu(); - } - } - } - }; - - void loadItems() { + private void loadMoreItems() { if (disposable != null) { disposable.dispose(); } - disposable = Observable.fromCallable(this::loadData) + disposable = Observable.fromCallable(this::loadMoreData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(data -> { progLoading.setVisibility(View.GONE); - onFragmentLoaded(data); + episodes.addAll(data); + onFragmentLoaded(episodes); }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - @NonNull - List loadData() { - return DBReader.getRecentlyPublishedEpisodes(RECENT_EPISODES_LIMIT); - } - - void removeNewFlagWithUndo(FeedItem item) { - if (item == null) { - return; - } - - Log.d(TAG, "removeNewFlagWithUndo(" + item.getId() + ")"); - if (disposable != null) { - disposable.dispose(); - } - // we're marking it as unplayed since the user didn't actually play it - // but they don't want it considered 'NEW' anymore - DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); - - final Handler h = new Handler(getActivity().getMainLooper()); - final Runnable r = () -> { - FeedMedia media = item.getMedia(); - if (media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) { - DBWriter.deleteFeedMediaOfItem(getActivity(), media.getId()); + private void showFilterDialog() { + FilterDialog filterDialog = new FilterDialog(getContext(), feedItemFilter) { + @Override + protected void updateFilter(Set filterValues) { + feedItemFilter = new FeedItemFilter(filterValues.toArray(new String[filterValues.size()])); + loadItems(); } }; - Snackbar snackbar = Snackbar.make(getView(), getString(R.string.removed_new_flag_label), - Snackbar.LENGTH_LONG); - snackbar.setAction(getString(R.string.undo), v -> { - DBWriter.markItemPlayed(FeedItem.NEW, item.getId()); - // don't forget to cancel the thing that's going to remove the media - h.removeCallbacks(r); - }); - snackbar.show(); - h.postDelayed(r, (int) Math.ceil(snackbar.getDuration() * 1.05f)); + filterDialog.openDialog(); + } + + @NonNull + @Override + protected List loadData() { + return feedItemFilter.filter( DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE)); + } + + List loadMoreData() { + return feedItemFilter.filter( DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE)); } } 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 4bebfe4c9..bb8f4df9a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -87,11 +87,10 @@ public class ChaptersFragment extends ListFragment { controller = null; } - private void scrollTo(int position) { - getListView().setSelection(position); - } - private int getCurrentChapter(Playable media) { + if (media == null || media.getChapters() == null || media.getChapters().size() == 0 || controller == null) { + return -1; + } int currentPosition = controller.getPosition(); List chapters = media.getChapters(); @@ -126,8 +125,10 @@ public class ChaptersFragment extends ListFragment { if (adapter != null) { adapter.setMedia(media); adapter.notifyDataSetChanged(); - if (media != null && media.getChapters() != null && media.getChapters().size() != 0) { - scrollTo(getCurrentChapter(media)); + + int positionOfCurrentChapter = getCurrentChapter(media); + if (positionOfCurrentChapter != -1) { + getListView().setSelection(positionOfCurrentChapter); } } } 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 0610bfd24..ca21df661 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java @@ -79,7 +79,7 @@ public class EpisodesFragment extends Fragment { public static class EpisodesPagerAdapter extends FragmentPagerAdapter { private final Resources resources; - private final AllEpisodesFragment[] fragments = { + private final EpisodesListFragment[] fragments = { new NewEpisodesFragment(), new AllEpisodesFragment(), new FavoriteEpisodesFragment() diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java new file mode 100644 index 000000000..b6e3d14dd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -0,0 +1,445 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.support.v7.widget.SimpleItemAnimator; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; + +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.event.DownloaderUpdate; +import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.view.EmptyViewHandler; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Shows unread or recently published episodes + */ +public abstract class EpisodesListFragment extends Fragment { + + public static final String TAG = "EpisodesListFragment"; + + private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE | + EventDistributor.UNREAD_ITEMS_UPDATE | + EventDistributor.PLAYER_STATUS_UPDATE; + + private static final String DEFAULT_PREF_NAME = "PrefAllEpisodesFragment"; + private static final String PREF_SCROLL_POSITION = "scroll_position"; + private static final String PREF_SCROLL_OFFSET = "scroll_offset"; + + RecyclerView recyclerView; + AllEpisodesRecycleAdapter listAdapter; + ProgressBar progLoading; + EmptyViewHandler emptyView; + + @NonNull + List episodes = new ArrayList<>(); + @NonNull + private List downloaderList = new ArrayList<>(); + + private boolean isUpdatingFeeds; + boolean isMenuInvalidationAllowed = false; + + protected Disposable disposable; + private LinearLayoutManager layoutManager; + protected TextView txtvInformation; + + boolean showOnlyNewEpisodes() { + return false; + } + + String getPrefName() { + return DEFAULT_PREF_NAME; + } + + @Override + public void onStart() { + super.onStart(); + setHasOptionsMenu(true); + EventDistributor.getInstance().register(contentUpdate); + EventBus.getDefault().register(this); + loadItems(); + } + + @Override + public void onResume() { + super.onResume(); + registerForContextMenu(recyclerView); + } + + @Override + public void onPause() { + super.onPause(); + saveScrollPosition(); + unregisterForContextMenu(recyclerView); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + EventDistributor.getInstance().unregister(contentUpdate); + if (disposable != null) { + disposable.dispose(); + } + } + + private void saveScrollPosition() { + int firstItem = layoutManager.findFirstVisibleItemPosition(); + View firstItemView = layoutManager.findViewByPosition(firstItem); + float topOffset; + if (firstItemView == null) { + topOffset = 0; + } else { + topOffset = firstItemView.getTop(); + } + + SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(PREF_SCROLL_POSITION, firstItem); + editor.putFloat(PREF_SCROLL_OFFSET, topOffset); + editor.apply(); + } + + private void restoreScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE); + int position = prefs.getInt(PREF_SCROLL_POSITION, 0); + float offset = prefs.getFloat(PREF_SCROLL_OFFSET, 0.0f); + if (position > 0 || offset > 0) { + layoutManager.scrollToPositionWithOffset(position, (int) offset); + // restore once, then forget + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(PREF_SCROLL_POSITION, 0); + editor.putFloat(PREF_SCROLL_OFFSET, 0.0f); + editor.apply(); + } + } + + private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = + () -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (!isAdded()) { + return; + } + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.episodes, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) requireActivity()).loadChildFragment(SearchFragment.newInstance(s)); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!super.onOptionsItemSelected(item)) { + switch (item.getItemId()) { + case R.id.refresh_item: + AutoUpdateManager.runImmediate(requireContext()); + return true; + case R.id.mark_all_read_item: + ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(), + R.string.mark_all_read_label, + R.string.mark_all_read_confirmation_msg) { + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.markAllItemsRead(); + Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show(); + } + }; + markAllReadConfirmationDialog.createNewDialog().show(); + return true; + case R.id.remove_all_new_flags_item: + ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(), + R.string.remove_all_new_flags_label, + R.string.remove_all_new_flags_confirmation_msg) { + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.removeAllNewFlags(); + Toast.makeText(getActivity(), R.string.removed_all_new_flags_msg, Toast.LENGTH_SHORT).show(); + } + }; + removeAllNewFlagsConfirmationDialog.createNewDialog().show(); + return true; + default: + return false; + } + } else { + return true; + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); + if (!getUserVisibleHint()) { + return false; + } + if (!isVisible()) { + return false; + } + if (item.getItemId() == R.id.share_item) { + return true; // avoids that the position is reset when we need it in the submenu + } + + if (listAdapter.getSelectedItem() == null) { + Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); + return super.onContextItemSelected(item); + } + FeedItem selectedItem = listAdapter.getSelectedItem(); + + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); + } + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.all_episodes_fragment, container, false); + txtvInformation = root.findViewById(R.id.txtvInformation); + + layoutManager = new LinearLayoutManager(getActivity()); + recyclerView = root.findViewById(android.R.id.list); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); + recyclerView.setVisibility(View.GONE); + + RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); + if (animator instanceof SimpleItemAnimator) { + ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); + } + + progLoading = root.findViewById(R.id.progLoading); + progLoading.setVisibility(View.VISIBLE); + + emptyView = new EmptyViewHandler(getContext()); + emptyView.attachToRecyclerView(recyclerView); + emptyView.setIcon(R.attr.feed); + emptyView.setTitle(R.string.no_all_episodes_head_label); + emptyView.setMessage(R.string.no_all_episodes_label); + + createRecycleAdapter(recyclerView, emptyView); + emptyView.hide(); + + return root; + } + + protected void onFragmentLoaded(List episodes) { + listAdapter.notifyDataSetChanged(); + + if (episodes.size() == 0) { + createRecycleAdapter(recyclerView, emptyView); + } + + restoreScrollPosition(); + requireActivity().invalidateOptionsMenu(); + } + + /** + * Currently, we need to recreate the list adapter in order to be able to undo last item via the + * snackbar. See #3084 for details. + */ + private void createRecycleAdapter(RecyclerView recyclerView, EmptyViewHandler emptyViewHandler) { + MainActivity mainActivity = (MainActivity) getActivity(); + listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, showOnlyNewEpisodes()); + listAdapter.setHasStableIds(true); + recyclerView.setAdapter(listAdapter); + emptyViewHandler.updateAdapter(listAdapter); + } + + private final AllEpisodesRecycleAdapter.ItemAccess itemAccess = new AllEpisodesRecycleAdapter.ItemAccess() { + + @Override + public int getCount() { + return episodes.size(); + } + + @Override + public FeedItem getItem(int position) { + if (0 <= position && position < episodes.size()) { + return episodes.get(position); + } + return null; + } + + @Override + public LongList getItemsIds() { + LongList ids = new LongList(episodes.size()); + for (FeedItem episode : episodes) { + ids.add(episode.getId()); + } + return ids; + } + + @Override + public int getItemDownloadProgressPercent(FeedItem item) { + for (Downloader downloader : downloaderList) { + DownloadRequest downloadRequest = downloader.getDownloadRequest(); + if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloadRequest.getFeedfileId() == item.getMedia().getId()) { + return downloadRequest.getProgressPercent(); + } + } + return 0; + } + + @Override + public boolean isInQueue(FeedItem item) { + return item != null && item.isTagged(FeedItem.TAG_QUEUE); + } + + @Override + public LongList getQueueIds() { + LongList queueIds = new LongList(); + for (FeedItem item : episodes) { + if (item.isTagged(FeedItem.TAG_QUEUE)) { + queueIds.add(item.getId()); + } + } + return queueIds; + } + + }; + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + for (FeedItem item : event.items) { + int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); + if (pos >= 0) { + episodes.remove(pos); + if (shouldUpdatedItemRemainInList(item)) { + episodes.add(pos, item); + listAdapter.notifyItemChanged(pos); + } else { + listAdapter.notifyItemRemoved(pos); + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (listAdapter != null) { + listAdapter.notifyCurrentlyPlayingItemChanged(event); + } + } + + protected boolean shouldUpdatedItemRemainInList(FeedItem item) { + return true; + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(DownloadEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + DownloaderUpdate update = event.update; + downloaderList = update.downloaders; + if (isMenuInvalidationAllowed && event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) { + requireActivity().invalidateOptionsMenu(); + } + if (update.mediaIds.length > 0) { + for (long mediaId : update.mediaIds) { + int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId); + if (pos >= 0) { + listAdapter.notifyItemChanged(pos); + } + } + } + } + + private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EVENTS) != 0) { + loadItems(); + if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) { + requireActivity().invalidateOptionsMenu(); + } + } + } + }; + + void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(this::loadData) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(data -> { + progLoading.setVisibility(View.GONE); + episodes = data; + onFragmentLoaded(episodes); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + @NonNull + protected abstract List loadData(); +} 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 348c73b92..9f136490a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -18,7 +18,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.event.ServiceEvent; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.service.playback.PlaybackService; @@ -28,6 +28,9 @@ import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * Fragment which is supposed to be displayed outside of the MediaplayerActivity @@ -138,6 +141,7 @@ public class ExternalPlayerFragment extends Fragment { controller = setupPlaybackController(); controller.init(); loadMediaInfo(); + EventBus.getDefault().register(this); } @Override @@ -147,6 +151,12 @@ public class ExternalPlayerFragment extends Fragment { controller.release(); controller = null; } + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + onPositionObserverUpdate(); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java index bb029b731..536ebd468 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java @@ -7,6 +7,7 @@ import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; import android.view.View; import android.view.ViewGroup; @@ -25,7 +26,7 @@ import de.danoeh.antennapod.core.storage.DBWriter; * Like 'EpisodesFragment' except that it only shows favorite episodes and * supports swiping to remove from favorites. */ -public class FavoriteEpisodesFragment extends AllEpisodesFragment { +public class FavoriteEpisodesFragment extends EpisodesListFragment { private static final String TAG = "FavoriteEpisodesFrag"; private static final String PREF_NAME = "PrefFavoriteEpisodesFragment"; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java similarity index 94% rename from app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java rename to app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 0c75af986..8fe0883b2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -29,6 +29,7 @@ import com.bumptech.glide.request.RequestOptions; import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.widget.IconTextView; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import org.apache.commons.lang3.Validate; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -79,7 +80,7 @@ import io.reactivex.schedulers.Schedulers; * Displays a list of FeedItems. */ @SuppressLint("ValidFragment") -public class ItemlistFragment extends ListFragment { +public class FeedItemlistFragment extends ListFragment { private static final String TAG = "ItemlistFragment"; private static final int EVENTS = EventDistributor.UNREAD_ITEMS_UPDATE @@ -120,8 +121,8 @@ public class ItemlistFragment extends ListFragment { * @param feedId The id of the feed to show * @return the newly created instance of an ItemlistFragment */ - public static ItemlistFragment newInstance(long feedId) { - ItemlistFragment i = new ItemlistFragment(); + public static FeedItemlistFragment newInstance(long feedId) { + FeedItemlistFragment i = new FeedItemlistFragment(); Bundle b = new Bundle(); b.putLong(ARGUMENT_FEED_ID, feedId); i.setArguments(b); @@ -195,6 +196,21 @@ public class ItemlistFragment extends ListFragment { final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); MenuItemUtils.adjustTextColor(getActivity(), sv); sv.setQueryHint(getString(R.string.search_hint)); + searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + menu.findItem(R.id.filter_items).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.findItem(R.id.episode_actions).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.findItem(R.id.refresh_item).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + getActivity().invalidateOptionsMenu(); + return true; + } + }); sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { @@ -308,7 +324,7 @@ public class ItemlistFragment extends ListFragment { contextMenu = menu; lastMenuInfo = (AdapterView.AdapterContextMenuInfo) menuInfo; - FeedItemMenuHandler.onPrepareMenu(contextMenuInterface, item, true, null); + FeedItemMenuHandler.onPrepareMenu(contextMenuInterface, item); } @Override @@ -325,7 +341,7 @@ public class ItemlistFragment extends ListFragment { return super.onContextItemSelected(item); } - return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); } @Override @@ -375,14 +391,21 @@ public class ItemlistFragment extends ListFragment { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; downloaderList = update.downloaders; - if (isUpdatingFeed != event.update.feedIds.length > 0) { + if (event.hasChangedFeedUpdateStatus(isUpdatingFeed)) { updateProgressBarVisibility(); } - if(adapter != null && update.mediaIds.length > 0) { + if (adapter != null && update.mediaIds.length > 0) { adapter.notifyDataSetChanged(); } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (adapter != null) { + adapter.notifyCurrentlyPlayingItemChanged(event, getListView()); + } + } + private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { @Override 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 5cf2c5eeb..bfca90b2a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -96,13 +96,7 @@ public class ItemDescriptionFragment extends Fragment { if (Timeline.isTimecodeLink(url)) { onTimecodeLinkSelected(url); } else { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - try { - startActivity(intent); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - return true; - } + IntentUtils.openInBrowser(getContext(), url); } return true; } @@ -159,11 +153,7 @@ public class ItemDescriptionFragment extends Fragment { if (selectedURL != null) { switch (item.getItemId()) { case R.id.open_in_browser_item: - Uri uri = Uri.parse(selectedURL); - final Intent intent = new Intent(Intent.ACTION_VIEW, uri); - if(IntentUtils.isCallable(getActivity(), intent)) { - getActivity().startActivity(intent); - } + IntentUtils.openInBrowser(getContext(), selectedURL); break; case R.id.share_url_item: ShareUtils.shareLink(getActivity(), selectedURL); 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 432ada44e..9395b0994 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -55,7 +55,6 @@ import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -211,10 +210,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { webvDescription.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - if(IntentUtils.isCallable(getActivity(), intent)) { - startActivity(intent); - } + IntentUtils.openInBrowser(getContext(), url); return true; } }); @@ -336,10 +332,10 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { inflater.inflate(R.menu.feeditem_options, menu); popupMenu = menu; if (item.hasMedia()) { - FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, null); + FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item); } else { // these are already available via button1 and button2 - FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, null, + FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, R.id.mark_read_item, R.id.visit_website_item); } } @@ -351,7 +347,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { openPodcast(); return true; default: - return FeedItemMenuHandler.onMenuItemClicked(getActivity(), menuItem.getItemId(), item); + return FeedItemMenuHandler.onMenuItemClicked(this, menuItem.getItemId(), item); } } @@ -446,15 +442,6 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { } } - FeedItem.State state = item.getState(); - if (butAction2Text == R.string.delete_label && state == FeedItem.State.PLAYING && PlaybackService.isRunning) { - butAction2.setEnabled(false); - butAction2.setAlpha(0.5f); - } else { - butAction2.setEnabled(true); - butAction2.setAlpha(1.0f); - } - if(butAction1Icon != null && butAction1Text != 0) { butAction1.setText(butAction1Icon +"\u0020\u0020" + getActivity().getString(butAction1Text)); Iconify.addIcons(butAction1); @@ -494,11 +481,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { if (selectedURL != null) { switch (item.getItemId()) { case R.id.open_in_browser_item: - Uri uri = Uri.parse(selectedURL); - final Intent intent = new Intent(Intent.ACTION_VIEW, uri); - if(IntentUtils.isCallable(getActivity(), intent)) { - getActivity().startActivity(intent); - } + IntentUtils.openInBrowser(getContext(), selectedURL); break; case R.id.share_url_item: ShareUtils.shareLink(getActivity(), selectedURL); @@ -543,7 +526,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { } private void openPodcast() { - Fragment fragment = ItemlistFragment.newInstance(item.getFeedId()); + Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId()); ((MainActivity)getActivity()).loadChildFragment(fragment); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index 1bf907aee..07667118d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -5,6 +5,7 @@ import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; +import android.view.Menu; import android.view.View; import android.view.ViewGroup; @@ -14,12 +15,13 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; /** * Like 'EpisodesFragment' except that it only shows new episodes and * supports swiping to mark as read. */ -public class NewEpisodesFragment extends AllEpisodesFragment { +public class NewEpisodesFragment extends EpisodesListFragment { public static final String TAG = "NewEpisodesFragment"; private static final String PREF_NAME = "PrefNewEpisodesFragment"; @@ -39,6 +41,12 @@ public class NewEpisodesFragment extends AllEpisodesFragment { return item.isNew(); } + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.remove_all_new_flags_item).setVisible(!episodes.isEmpty()); + } + @NonNull @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -55,7 +63,7 @@ public class NewEpisodesFragment extends AllEpisodesFragment { @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { AllEpisodesRecycleAdapter.Holder holder = (AllEpisodesRecycleAdapter.Holder) viewHolder; - removeNewFlagWithUndo(holder.getFeedItem()); + FeedItemMenuHandler.removeNewFlagWithUndo(NewEpisodesFragment.this, holder.getFeedItem()); } @Override 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 0fe413954..ff1e9a28e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -7,6 +7,7 @@ import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; @@ -19,11 +20,17 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.ProgressBar; import android.widget.TextView; import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import java.util.List; import de.danoeh.antennapod.R; @@ -35,14 +42,12 @@ import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.EventDistributor; -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.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; @@ -50,18 +55,15 @@ import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.QueueSorter; import de.danoeh.antennapod.core.util.SortOrder; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.MenuItemUtils; - import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DELETE; import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REMOVE_FROM_QUEUE; @@ -91,10 +93,12 @@ public class QueueFragment extends Fragment { private static final String PREFS = "QueueFragment"; private static final String PREF_SCROLL_POSITION = "scroll_position"; private static final String PREF_SCROLL_OFFSET = "scroll_offset"; + private static final String PREF_SHOW_LOCK_WARNING = "show_lock_warning"; private Disposable disposable; private LinearLayoutManager layoutManager; private ItemTouchHelper itemTouchHelper; + private SharedPreferences prefs; @Override @@ -102,6 +106,7 @@ public class QueueFragment extends Fragment { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); + prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); } @Override @@ -196,8 +201,8 @@ public class QueueFragment extends Fragment { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; downloaderList = update.downloaders; - if (isUpdatingFeeds != update.feedIds.length > 0) { - getActivity().supportInvalidateOptionsMenu(); + if (event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) { + getActivity().invalidateOptionsMenu(); } if (recyclerAdapter != null && update.mediaIds.length > 0) { for (long mediaId : update.mediaIds) { @@ -209,6 +214,13 @@ public class QueueFragment extends Fragment { } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (recyclerAdapter != null) { + recyclerAdapter.notifyCurrentlyPlayingItemChanged(event); + } + } + private void saveScrollPosition() { int firstItem = layoutManager.findFirstVisibleItemPosition(); View firstItemView = layoutManager.findViewByPosition(firstItem); @@ -219,15 +231,13 @@ public class QueueFragment extends Fragment { topOffset = firstItemView.getTop(); } - SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(PREF_SCROLL_POSITION, firstItem); - editor.putFloat(PREF_SCROLL_OFFSET, topOffset); - editor.commit(); + prefs.edit() + .putInt(PREF_SCROLL_POSITION, firstItem) + .putFloat(PREF_SCROLL_OFFSET, topOffset) + .apply(); } private void restoreScrollPosition() { - SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); int position = prefs.getInt(PREF_SCROLL_POSITION, 0); float offset = prefs.getFloat(PREF_SCROLL_OFFSET, 0.0f); if (position > 0 || offset > 0) { @@ -299,25 +309,10 @@ public class QueueFragment extends Fragment { if (!super.onOptionsItemSelected(item)) { switch (item.getItemId()) { case R.id.queue_lock: - boolean newLockState = !UserPreferences.isQueueLocked(); - UserPreferences.setQueueLocked(newLockState); - getActivity().supportInvalidateOptionsMenu(); - if (recyclerAdapter != null) { - recyclerAdapter.setLocked(newLockState); - } - if (newLockState) { - Snackbar.make(getActivity().findViewById(R.id.content), R.string - .queue_locked, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(getActivity().findViewById(R.id.content), R.string - .queue_unlocked, Snackbar.LENGTH_SHORT).show(); - } + toggleQueueLock(); return true; case R.id.refresh_item: - List feeds = ((MainActivity) getActivity()).getFeeds(); - if (feeds != null) { - DBTasks.refreshAllFeeds(getActivity(), feeds); - } + AutoUpdateManager.runImmediate(requireContext()); return true; case R.id.clear_queue: // make sure the user really wants to clear the queue @@ -378,8 +373,10 @@ public class QueueFragment extends Fragment { if (keepSortedNew) { SortOrder sortOrder = UserPreferences.getQueueKeepSortedOrder(); QueueSorter.sort(sortOrder, true); - recyclerAdapter.setLocked(true); - } else { + if (recyclerAdapter != null) { + recyclerAdapter.setLocked(true); + } + } else if (recyclerAdapter != null) { recyclerAdapter.setLocked(UserPreferences.isQueueLocked()); } getActivity().invalidateOptionsMenu(); @@ -392,6 +389,48 @@ public class QueueFragment extends Fragment { } } + private void toggleQueueLock() { + boolean isLocked = UserPreferences.isQueueLocked(); + if (isLocked) { + setQueueLocked(false); + } else { + boolean shouldShowLockWarning = prefs.getBoolean(PREF_SHOW_LOCK_WARNING, true); + if (!shouldShowLockWarning) { + setQueueLocked(true); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(R.string.lock_queue); + builder.setMessage(R.string.queue_lock_warning); + + View view = View.inflate(getContext(), R.layout.checkbox_do_not_show_again, null); + CheckBox checkDoNotShowAgain = view.findViewById(R.id.checkbox_do_not_show_again); + builder.setView(view); + + builder.setPositiveButton(R.string.lock_queue, (dialog, which) -> { + prefs.edit().putBoolean(PREF_SHOW_LOCK_WARNING, !checkDoNotShowAgain.isChecked()).apply(); + setQueueLocked(true); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + } + } + } + + private void setQueueLocked(boolean locked) { + UserPreferences.setQueueLocked(locked); + getActivity().supportInvalidateOptionsMenu(); + if (recyclerAdapter != null) { + recyclerAdapter.setLocked(locked); + } + if (locked) { + Snackbar.make(getActivity().findViewById(R.id.content), R.string + .queue_locked, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(getActivity().findViewById(R.id.content), R.string + .queue_unlocked, Snackbar.LENGTH_SHORT).show(); + } + } + /** * This method is called if the user clicks on a sort order menu item. * @@ -428,7 +467,7 @@ public class QueueFragment extends Fragment { DBWriter.moveQueueItemToBottom(selectedItem.getId(), true); return true; default: - return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); } } @@ -503,7 +542,7 @@ public class QueueFragment extends Fragment { @Override public boolean isLongPressDragEnabled() { - return !UserPreferences.isQueueLocked(); + return false; } @Override @@ -596,7 +635,7 @@ public class QueueFragment extends Fragment { String info = queue.size() + getString(R.string.episodes_suffix); if(queue.size() > 0) { long timeLeft = 0; - float playbackSpeed = Float.valueOf(UserPreferences.getPlaybackSpeed()); + float playbackSpeed = UserPreferences.getPlaybackSpeed(); for(FeedItem item : queue) { if(item.getMedia() != null) { timeLeft += @@ -604,7 +643,7 @@ public class QueueFragment extends Fragment { / playbackSpeed); } } - info += " \u2022 "; + info += " • "; info += getString(R.string.time_left_label); info += Converter.getDurationStringLocalized(getActivity(), timeLeft); } 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 15c6052a9..ed315050b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -25,19 +25,28 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.SubscriptionsAdapter; import de.danoeh.antennapod.core.asynctask.FeedRemover; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.service.download.DownloadService; 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.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.RenameFeedDialog; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * Fragment for displaying feed subscriptions @@ -56,6 +65,7 @@ public class SubscriptionFragment extends Fragment { private SubscriptionsAdapter subscriptionAdapter; private int mPosition = -1; + private boolean isUpdatingFeeds = false; private Disposable disposable; private SharedPreferences prefs; @@ -89,6 +99,8 @@ public class SubscriptionFragment extends Fragment { menu.findItem(R.id.subscription_num_columns_3).setChecked(columns == 3); menu.findItem(R.id.subscription_num_columns_4).setChecked(columns == 4); menu.findItem(R.id.subscription_num_columns_5).setChecked(columns == 5); + + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } @Override @@ -97,6 +109,9 @@ public class SubscriptionFragment extends Fragment { return true; } switch (item.getItemId()) { + case R.id.refresh_item: + AutoUpdateManager.runImmediate(requireContext()); + return true; case R.id.subscription_num_columns_2: setColumnNumber(2); return true; @@ -136,6 +151,7 @@ public class SubscriptionFragment extends Fragment { public void onStart() { super.onStart(); EventDistributor.getInstance().register(contentUpdate); + EventBus.getDefault().register(this); loadSubscriptions(); } @@ -143,6 +159,7 @@ public class SubscriptionFragment extends Fragment { public void onStop() { super.onStop(); EventDistributor.getInstance().unregister(contentUpdate); + EventBus.getDefault().unregister(this); if(disposable != null) { disposable.dispose(); } @@ -278,6 +295,17 @@ public class SubscriptionFragment extends Fragment { } }; + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(DownloadEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) { + getActivity().invalidateOptionsMenu(); + } + } + + private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = + () -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); + private final SubscriptionsAdapter.ItemAccess itemAccess = new SubscriptionsAdapter.ItemAccess() { @Override public int getCount() { 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 a04615a00..d0c209326 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 @@ -1,29 +1,41 @@ package de.danoeh.antennapod.fragment.preferences; +import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; import android.support.v7.preference.CheckBoxPreference; import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceFragmentCompat; import android.support.v7.preference.PreferenceScreen; import android.util.Log; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.UserPreferences; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { private static final String TAG = "AutoDnldPrefFragment"; + + private static final int LOCATION_PERMISSION_REQUEST_CODE = 1; + private static final String PREF_KEY_LOCATION_PERMISSION_REQUEST_PROMPT = "prefAutoDownloadWifiFilterAndroid10PermissionPrompt"; + private CheckBoxPreference[] selectedNetworks; + private Preference prefPermissionRequestPromptOnAndroid10 = null; + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.preferences_autodownload); @@ -175,10 +187,65 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { } private void setSelectedNetworksEnabled(boolean b) { + if (showPermissionRequestPromptOnAndroid10IfNeeded(b)) { + return; + } + if (selectedNetworks != null) { for (Preference p : selectedNetworks) { p.setEnabled(b); } } } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode != LOCATION_PERMISSION_REQUEST_CODE) { + return; + } + if (permissions.length > 0 && permissions[0].equals(Manifest.permission.ACCESS_FINE_LOCATION) && + grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + buildAutodownloadSelectedNetworksPreference(); + } + } + + private boolean showPermissionRequestPromptOnAndroid10IfNeeded(boolean wifiFilterEnabled) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + return false; + } + + // Cases Android 10(Q) or later + if (prefPermissionRequestPromptOnAndroid10 != null) { + getPreferenceScreen().removePreference(prefPermissionRequestPromptOnAndroid10); + prefPermissionRequestPromptOnAndroid10 = null; + } + + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED) { + return false; + } + + // Case location permission not yet granted, permission-specific UI is needed + if (!wifiFilterEnabled) { + // Don't show the UI when WiFi filter disabled. + // it still return true, so that the caller knows + // it does not have required permission, and will not invoke codes that require so. + return true; + } + + Preference pref = new Preference(requireActivity()); + pref.setKey(PREF_KEY_LOCATION_PERMISSION_REQUEST_PROMPT); + pref.setTitle(R.string.autodl_wifi_filter_permission_title); + pref.setSummary(R.string.autodl_wifi_filter_permission_message); + pref.setIcon(R.drawable.ic_warning_red); + pref.setOnPreferenceClickListener(preference -> { + requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST_CODE); + return true; + }); + pref.setPersistent(false); + getPreferenceScreen().addPreference(pref); + prefPermissionRequestPromptOnAndroid10 = pref; + return true; + } + } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java index 701d21ce0..9f36e1355 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -1,27 +1,21 @@ package de.danoeh.antennapod.fragment.preferences; import android.content.ActivityNotFoundException; -import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.support.v4.content.FileProvider; import android.support.v7.app.AppCompatActivity; import android.support.v7.preference.PreferenceFragmentCompat; import android.util.Log; import android.widget.Toast; import com.bytehamster.lib.preferencesearch.SearchConfiguration; import com.bytehamster.lib.preferencesearch.SearchPreference; -import de.danoeh.antennapod.CrashReportWriter; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AboutActivity; +import de.danoeh.antennapod.activity.BugReportActivity; import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.activity.StatisticsActivity; - -import java.util.List; +import de.danoeh.antennapod.core.util.IntentUtils; public class MainPreferencesFragment extends PreferenceFragmentCompat { private static final String TAG = "MainPreferencesFragment"; @@ -31,9 +25,9 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_SCREEN_NETWORK = "prefScreenNetwork"; private static final String PREF_SCREEN_INTEGRATIONS = "prefScreenIntegrations"; private static final String PREF_SCREEN_STORAGE = "prefScreenStorage"; - private static final String PREF_KNOWN_ISSUES = "prefKnownIssues"; private static final String PREF_FAQ = "prefFaq"; - private static final String PREF_SEND_CRASH_REPORT = "prefSendCrashReport"; + private static final String PREF_VIEW_MAILING_LIST = "prefViewMailingList"; + private static final String PREF_SEND_BUG_REPORT = "prefSendBugReport"; private static final String STATISTICS = "statistics"; private static final String PREF_ABOUT = "prefAbout"; @@ -78,47 +72,18 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { return true; } ); - findPreference(PREF_KNOWN_ISSUES).setOnPreferenceClickListener(preference -> { - openInBrowser("https://github.com/AntennaPod/AntennaPod/labels/bug"); - return true; - }); findPreference(PREF_FAQ).setOnPreferenceClickListener(preference -> { - openInBrowser("http://antennapod.org/faq.html"); + IntentUtils.openInBrowser(getContext(), "https://antennapod.org/faq.html"); return true; }); - findPreference(PREF_SEND_CRASH_REPORT).setOnPreferenceClickListener(preference -> { - Context context = getActivity().getApplicationContext(); - Intent emailIntent = new Intent(Intent.ACTION_SEND); - emailIntent.setType("text/plain"); - emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"Martin.Fietz@gmail.com"}); - emailIntent.putExtra(Intent.EXTRA_SUBJECT, "AntennaPod Crash Report"); - emailIntent.putExtra(Intent.EXTRA_TEXT, "Please describe what you were doing when the app crashed"); - // the attachment - Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority), - CrashReportWriter.getFile()); - emailIntent.putExtra(Intent.EXTRA_STREAM, fileUri); - emailIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - String intentTitle = getActivity().getString(R.string.send_email); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List resInfoList = context.getPackageManager().queryIntentActivities(emailIntent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - getActivity().startActivity(Intent.createChooser(emailIntent, intentTitle)); + findPreference(PREF_VIEW_MAILING_LIST).setOnPreferenceClickListener(preference -> { + IntentUtils.openInBrowser(getContext(), "https://groups.google.com/forum/#!forum/antennapod"); + return true; + }); + findPreference(PREF_SEND_BUG_REPORT).setOnPreferenceClickListener(preference -> { + startActivity(new Intent(getActivity(), BugReportActivity.class)); return true; }); - } - - private void openInBrowser(String url) { - try { - Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(myIntent); - } catch (ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.pref_no_browser_found, Toast.LENGTH_LONG).show(); - Log.e(TAG, Log.getStackTraceString(e)); - } } private void setupSearch() { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java index b4226b546..e36476c6f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java @@ -4,6 +4,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -13,13 +14,16 @@ import android.os.Build; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.FileProvider; +import android.support.v4.provider.DocumentFile; import android.support.v7.app.AlertDialog; import android.support.v7.preference.PreferenceFragmentCompat; import android.util.Log; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DirectoryChooserActivity; import de.danoeh.antennapod.activity.ImportExportActivity; import de.danoeh.antennapod.activity.OpmlImportFromPathActivity; +import de.danoeh.antennapod.asynctask.DocumentFileExportWorker; import de.danoeh.antennapod.asynctask.ExportWorker; import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.core.export.html.HtmlWriter; @@ -45,6 +49,12 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; private static final int PERMISSION_REQUEST_EXTERNAL_STORAGE = 41; + private static final int CHOOSE_OPML_EXPORT_PATH = 1; + private static final String DEFAULT_OPML_OUTPUT_NAME = "antennapod-feeds.opml"; + private static final String CONTENT_TYPE_OPML = "text/x-opml"; + private static final int CHOOSE_HTML_EXPORT_PATH = 2; + private static final String DEFAULT_HTML_OUTPUT_NAME = "antennapod-feeds.html"; + private static final String CONTENT_TYPE_HTML = "text/html"; private Disposable disposable; @Override @@ -59,6 +69,14 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { setDataFolderText(); } + @Override + public void onStop() { + super.onStop(); + if (disposable != null) { + disposable.dispose(); + } + } + private void setupStorageScreen() { final Activity activity = getActivity(); @@ -69,9 +87,16 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { } ); findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( - preference -> export(new OpmlWriter())); + preference -> { + openOpmlExportPathPicker(); + return true; + } + ); findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( - preference -> export(new HtmlWriter())); + preference -> { + openHtmlExportPathPicker(); + return true; + }); findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( preference -> { activity.startActivity(new Intent(activity, OpmlImportFromPathActivity.class)); @@ -129,52 +154,65 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { } private boolean export(ExportWriter exportWriter) { + return export(exportWriter, null); + } + + private boolean export(ExportWriter exportWriter, final Uri uri) { Context context = getActivity(); final ProgressDialog progressDialog = new ProgressDialog(context); progressDialog.setMessage(context.getString(R.string.exporting_label)); progressDialog.setIndeterminate(true); progressDialog.show(); - final AlertDialog.Builder alert = new AlertDialog.Builder(context) - .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); - Observable observable = new ExportWorker(exportWriter).exportObservable(); - disposable = observable.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(output -> { - alert.setTitle(R.string.export_success_title); - String message = context.getString(R.string.export_success_sum, output.toString()); - alert.setMessage(message); - alert.setPositiveButton(R.string.send_label, (dialog, which) -> { + if (uri == null) { + Observable observable = new ExportWorker(exportWriter).exportObservable(); + disposable = observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(output -> { Uri fileUri = FileProvider.getUriForFile(context.getApplicationContext(), context.getString(R.string.provider_authority), output); - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, - context.getResources().getText(R.string.opml_export_label)); - sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri); - sendIntent.setType("text/plain"); - sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List resInfoList = context.getPackageManager().queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - context.startActivity(Intent.createChooser(sendIntent, - context.getResources().getText(R.string.send_label))); - }); - alert.create().show(); - }, error -> { - alert.setTitle(R.string.export_error_label); - alert.setMessage(error.getMessage()); - alert.show(); - }, progressDialog::dismiss); + showExportSuccessDialog(context.getString(R.string.export_success_sum, output.toString()), fileUri); + }, this::showExportErrorDialog, progressDialog::dismiss); + } else { + Observable observable = new DocumentFileExportWorker(exportWriter, context, uri).exportObservable(); + disposable = observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(output -> { + showExportSuccessDialog(context.getString(R.string.export_success_sum, output.getUri()), output.getUri()); + }, this::showExportErrorDialog, progressDialog::dismiss); + } return true; } - public void unsubscribeExportSubscription() { - if (disposable != null) { - disposable.dispose(); - } + private void showExportSuccessDialog(final String message, final Uri streamUri) { + final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()) + .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); + alert.setTitle(R.string.export_success_title); + alert.setMessage(message); + alert.setPositiveButton(R.string.send_label, (dialog, which) -> { + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.opml_export_label)); + sendIntent.putExtra(Intent.EXTRA_STREAM, streamUri); + sendIntent.setType("text/plain"); + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + List resInfoList = getContext().getPackageManager() + .queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + getContext().grantUriPermission(packageName, streamUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } + getContext().startActivity(Intent.createChooser(sendIntent, getString(R.string.send_label))); + }); + alert.create().show(); + } + + private void showExportErrorDialog(final Throwable error) { + final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()) + .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); + alert.setTitle(R.string.export_error_label); + alert.setMessage(error.getMessage()); + alert.show(); } @SuppressLint("NewApi") @@ -184,22 +222,22 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { String dir = data.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); File path; - if(dir != null) { + if (dir != null) { path = new File(dir); } else { path = getActivity().getExternalFilesDir(null); } String message = null; - final Context context= getActivity().getApplicationContext(); - if(!path.exists()) { + final Context context = getActivity().getApplicationContext(); + if (!path.exists()) { message = String.format(context.getString(R.string.folder_does_not_exist_error), dir); - } else if(!path.canRead()) { + } else if (!path.canRead()) { message = String.format(context.getString(R.string.folder_not_readable_error), dir); - } else if(!path.canWrite()) { + } else if (!path.canWrite()) { message = String.format(context.getString(R.string.folder_not_writable_error), dir); } - if(message == null) { + if (message == null) { Log.d(TAG, "Setting data folder: " + dir); UserPreferences.setDataFolder(dir); setDataFolderText(); @@ -210,6 +248,16 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { ab.show(); } } + + if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_OPML_EXPORT_PATH) { + Uri uri = data.getData(); + export(new OpmlWriter(), uri); + } + + if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_HTML_EXPORT_PATH) { + Uri uri = data.getData(); + export(new HtmlWriter(), uri); + } } private void setDataFolderText() { @@ -231,6 +279,50 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { activity.startActivityForResult(intent, DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); } + private void openOpmlExportPathPicker() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(CONTENT_TYPE_OPML) + .putExtra(Intent.EXTRA_TITLE, DEFAULT_OPML_OUTPUT_NAME); + + // Creates an implicit intent to launch a file manager which lets + // the user choose a specific directory to export to. + try { + startActivityForResult(intentPickAction, CHOOSE_OPML_EXPORT_PATH); + return; + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + } + + // If we are using a SDK lower than API 21 or the implicit intent failed + // fallback to the legacy export process + export(new OpmlWriter()); + } + + private void openHtmlExportPathPicker() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(CONTENT_TYPE_HTML) + .putExtra(Intent.EXTRA_TITLE, DEFAULT_HTML_OUTPUT_NAME); + + // Creates an implicit intent to launch a file manager which lets + // the user choose a specific directory to export to. + try { + startActivityForResult(intentPickAction, CHOOSE_HTML_EXPORT_PATH); + return; + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + } + + // If we are using a SDK lower than API 21 or the implicit intent failed + // fallback to the legacy export process + export(new HtmlWriter()); + } + private void showChooseDataFolderDialog() { ChooseDataFolderDialog.showDialog( getActivity(), new ChooseDataFolderDialog.RunnableWithString() { diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index 156657a00..add62b480 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -3,7 +3,10 @@ package de.danoeh.antennapod.menuhandler; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.support.annotation.Nullable; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.design.widget.Snackbar; +import android.support.v4.app.Fragment; import android.util.Log; import android.widget.Toast; @@ -18,7 +21,6 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.ShareUtils; /** @@ -51,35 +53,21 @@ public class FeedItemMenuHandler { * @param mi An instance of MenuInterface that the method uses to change a * MenuItem's visibility * @param selectedItem The FeedItem for which the menu is supposed to be prepared - * @param showExtendedMenu True if MenuItems that let the user share information about - * the FeedItem and visit its website should be set visible. This - * parameter should be set to false if the menu space is limited. - * @param queueAccess Used for testing if the queue contains the selected item; only used for - * move to top/bottom in the queue * @return Returns true if selectedItem is not null. */ public static boolean onPrepareMenu(MenuInterface mi, - FeedItem selectedItem, - boolean showExtendedMenu, - @Nullable LongList queueAccess) { + FeedItem selectedItem) { if (selectedItem == null) { return false; } boolean hasMedia = selectedItem.getMedia() != null; boolean isPlaying = hasMedia && selectedItem.getState() == FeedItem.State.PLAYING; - boolean keepSorted = UserPreferences.isQueueKeepSorted(); if (!isPlaying) { mi.setItemVisibility(R.id.skip_episode_item, false); } boolean isInQueue = selectedItem.isTagged(FeedItem.TAG_QUEUE); - if (queueAccess == null || queueAccess.size() == 0 || queueAccess.get(0) == selectedItem.getId() || keepSorted) { - mi.setItemVisibility(R.id.move_to_top_item, false); - } - if (queueAccess == null || queueAccess.size() == 0 || queueAccess.get(queueAccess.size()-1) == selectedItem.getId() || keepSorted) { - mi.setItemVisibility(R.id.move_to_bottom_item, false); - } if (!isInQueue) { mi.setItemVisibility(R.id.remove_from_queue_item, false); } @@ -87,12 +75,12 @@ public class FeedItemMenuHandler { mi.setItemVisibility(R.id.add_to_queue_item, false); } - if (!showExtendedMenu || !ShareUtils.hasLinkToShare(selectedItem)) { + if (!ShareUtils.hasLinkToShare(selectedItem)) { mi.setItemVisibility(R.id.visit_website_item, false); mi.setItemVisibility(R.id.share_link_item, false); mi.setItemVisibility(R.id.share_link_with_position_item, false); } - if (!showExtendedMenu || !hasMedia || selectedItem.getMedia().getDownload_url() == null) { + if (!hasMedia || selectedItem.getMedia().getDownload_url() == null) { mi.setItemVisibility(R.id.share_download_url_item, false); mi.setItemVisibility(R.id.share_download_url_with_position_item, false); } @@ -104,6 +92,7 @@ public class FeedItemMenuHandler { boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists(); mi.setItemVisibility(R.id.share_file, fileDownloaded); + mi.setItemVisibility(R.id.remove_new_flag_item, selectedItem.isNew()); if (selectedItem.isPlayed()) { mi.setItemVisibility(R.id.mark_read_item, false); } else { @@ -114,7 +103,7 @@ public class FeedItemMenuHandler { mi.setItemVisibility(R.id.reset_position, false); } - if(!UserPreferences.isEnableAutodownload()) { + if(!UserPreferences.isEnableAutodownload() || fileDownloaded) { mi.setItemVisibility(R.id.activate_auto_download, false); mi.setItemVisibility(R.id.deactivate_auto_download, false); } else if(selectedItem.getAutoDownload()) { @@ -141,10 +130,8 @@ public class FeedItemMenuHandler { */ public static boolean onPrepareMenu(MenuInterface mi, FeedItem selectedItem, - boolean showExtendedMenu, - LongList queueAccess, int... excludeIds) { - boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess); + boolean rc = onPrepareMenu(mi, selectedItem); if (rc && excludeIds != null) { for (int id : excludeIds) { mi.setItemVisibility(id, false); @@ -153,8 +140,16 @@ public class FeedItemMenuHandler { return rc; } - public static boolean onMenuItemClicked(Context context, int menuItemId, - FeedItem selectedItem) { + /** + * Default menu handling for the given FeedItem. + * + * A Fragment instance, (rather than the more generic Context), is needed as a parameter + * to support some UI operations, e.g., creating a Snackbar. + */ + public static boolean onMenuItemClicked(@NonNull Fragment fragment, int menuItemId, + @NonNull FeedItem selectedItem) { + + @NonNull Context context = fragment.requireContext(); switch (menuItemId) { case R.id.skip_episode_item: IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SKIP_CURRENT_EPISODE); @@ -162,6 +157,9 @@ public class FeedItemMenuHandler { case R.id.remove_item: DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); break; + case R.id.remove_new_flag_item: + removeNewFlagWithUndo(fragment, selectedItem); + break; case R.id.mark_read_item: selectedItem.setPlayed(true); DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, false); @@ -216,14 +214,7 @@ public class FeedItemMenuHandler { DBWriter.setFeedItemAutoDownload(selectedItem, false); break; case R.id.visit_website_item: - Uri uri = Uri.parse(FeedItemUtil.getLinkWithFallback(selectedItem)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - if(IntentUtils.isCallable(context, intent)) { - context.startActivity(intent); - } else { - Toast.makeText(context, context.getString(R.string.download_error_malformed_url), - Toast.LENGTH_SHORT).show(); - } + IntentUtils.openInBrowser(context, FeedItemUtil.getLinkWithFallback(selectedItem)); break; case R.id.share_link_item: ShareUtils.shareFeedItemLink(context, selectedItem); @@ -249,4 +240,39 @@ public class FeedItemMenuHandler { return true; } + /** + * Remove new flag with additional UI logic to allow undo with Snackbar. + * + * Undo is useful for Remove new flag, given there is no UI to undo it otherwise + * ,i.e., there is (context) menu item for add new flag + */ + public static void removeNewFlagWithUndo(@NonNull Fragment fragment, FeedItem item) { + if (item == null) { + return; + } + + Log.d(TAG, "removeNewFlagWithUndo(" + item.getId() + ")"); + // we're marking it as unplayed since the user didn't actually play it + // but they don't want it considered 'NEW' anymore + DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); + + final Handler h = new Handler(fragment.requireContext().getMainLooper()); + final Runnable r = () -> { + FeedMedia media = item.getMedia(); + if (media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) { + DBWriter.deleteFeedMediaOfItem(fragment.requireContext(), media.getId()); + } + }; + + Snackbar snackbar = Snackbar.make(fragment.getView(), fragment.getString(R.string.removed_new_flag_label), + Snackbar.LENGTH_LONG); + snackbar.setAction(fragment.getString(R.string.undo), v -> { + DBWriter.markItemPlayed(FeedItem.NEW, item.getId()); + // don't forget to cancel the thing that's going to remove the media + h.removeCallbacks(r); + }); + snackbar.show(); + h.postDelayed(r, (int) Math.ceil(snackbar.getDuration() * 1.05f)); + } + } diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index 0928cfd62..dbb3b6e7b 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -20,11 +20,13 @@ import java.util.Set; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.dialog.FilterDialog; /** * Handles interactions with the FeedItemMenu. @@ -84,14 +86,7 @@ public class FeedMenuHandler { conDialog.createNewDialog().show(); break; case R.id.visit_website_item: - Uri uri = Uri.parse(selectedFeed.getLink()); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - if(IntentUtils.isCallable(context, intent)) { - context.startActivity(intent); - } else { - Toast.makeText(context, context.getString(R.string.download_error_malformed_url), - Toast.LENGTH_SHORT).show(); - } + IntentUtils.openInBrowser(context, selectedFeed.getLink()); break; case R.id.share_link_item: ShareUtils.shareFeedlink(context, selectedFeed); @@ -105,42 +100,15 @@ public class FeedMenuHandler { return true; } - private static void showFilterDialog(final Context context, final Feed feed) { - final String[] items = context.getResources().getStringArray(R.array.episode_filter_options); - final String[] values = context.getResources().getStringArray(R.array.episode_filter_values); - final boolean[] checkedItems = new boolean[items.length]; - - final Set filter = new HashSet<>(Arrays.asList(feed.getItemFilter().getValues())); - Iterator it = filter.iterator(); - while(it.hasNext()) { - // make sure we have no empty strings in the filter list - if(TextUtils.isEmpty(it.next())) { - it.remove(); + private static void showFilterDialog(Context context, Feed selectedFeed) { + FilterDialog filterDialog = new FilterDialog(context, selectedFeed.getItemFilter()) { + @Override + protected void updateFilter(Set filterValues) { + selectedFeed.setItemFilter(filterValues.toArray(new String[filterValues.size()])); + DBWriter.setFeedItemsFilter(selectedFeed.getId(), filterValues); } - } - for(int i=0; i < values.length; i++) { - String value = values[i]; - if(filter.contains(value)) { - checkedItems[i] = true; - } - } - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.filter); - builder.setMultiChoiceItems(items, checkedItems, (dialog, which, isChecked) -> { - if (isChecked) { - filter.add(values[which]); - } else { - filter.remove(values[which]); - } - }); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - feed.setItemFilter(filter.toArray(new String[filter.size()])); - DBWriter.setFeedItemsFilter(feed.getId(), filter); - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); + }; + filterDialog.openDialog(); } - } 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 e56703598..6392d0535 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -3,27 +3,31 @@ package de.danoeh.antennapod.preferences; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; + import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.core.util.gui.NotificationUtils; public class PreferenceUpgrader { - private static final String PREF_CONFIGURED_VERSION = "configuredVersion"; - private static final String PREF_NAME = "PreferenceUpgrader"; + private static final String PREF_CONFIGURED_VERSION = "version_code"; + private static final String PREF_NAME = "app_version"; private static SharedPreferences prefs; public static void checkUpgrades(Context context) { prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences upgraderPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - int oldVersion = upgraderPrefs.getInt(PREF_CONFIGURED_VERSION, 1070200); + int oldVersion = upgraderPrefs.getInt(PREF_CONFIGURED_VERSION, -1); int newVersion = BuildConfig.VERSION_CODE; if (oldVersion != newVersion) { NotificationUtils.createChannels(context); + AutoUpdateManager.restartUpdateAlarm(); - upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply(); upgrade(oldVersion); + upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply(); } } @@ -41,12 +45,8 @@ public class PreferenceUpgrader { } } if (oldVersion < 1070300) { - UserPreferences.restartUpdateAlarm(); - - if (UserPreferences.getMediaPlayer().equals("builtin")) { - prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, - UserPreferences.PREF_MEDIA_PLAYER_EXOPLAYER).apply(); - } + prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, + UserPreferences.PREF_MEDIA_PLAYER_EXOPLAYER).apply(); if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) { UserPreferences.setAllowMobileAutoDownload(true); @@ -65,5 +65,13 @@ public class PreferenceUpgrader { break; } } + if (oldVersion < 1070400) { + int theme = UserPreferences.getTheme(); + if (theme == R.style.Theme_AntennaPod_Light) { + prefs.edit().putString(UserPreferences.PREF_THEME, "system").apply(); + } + + UserPreferences.setQueueLocked(false); + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java index 03958508d..5fa6588d9 100644 --- a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java +++ b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java @@ -46,7 +46,7 @@ public class SPAUtil { SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, true); - editor.commit(); + editor.apply(); return true; } else { @@ -63,7 +63,7 @@ public class SPAUtil { SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(c.getApplicationContext()).edit(); editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, false); - editor.commit(); + editor.apply(); } } } 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 f312ea559..c49417c3e 100644 --- a/app/src/main/play/listings/de-DE/full-description.txt +++ b/app/src/main/play/listings/de-DE/full-description.txt @@ -1,12 +1,12 @@ 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. Reduziere Aufwand, Stromverbrauch und Datenverbrauch durch die Kontrolle der Downloads (bestimmte Uhrzeiten, Intervalle, WiFi-Netze) und des Löschens (basierend auf deinen Favoriten und weiteren Einstellungen). -Aber am wichtigsten: Downloade, streame oder füge Episoden zur Abspielliste hinzu und genieße sie mit einstellbarer Abspielgeschwindigkeit, Unterstützung von Kapiteln und Schlummerfunktion. Mit Flattr kannst du den Podcastern sogar deine Wertschätzung zeigen. +Aber am wichtigsten: Downloade, streame oder füge Episoden zur Abspielliste hinzu und genieße sie mit einstellbarer Abspielgeschwindigkeit, Unterstützung von Kapiteln und Schlummerfunktion. AntennaPod ist, von Podcast-Enthusiasten gemacht, frei im Sinne des Wortes: Open Source, keine Kosten, keine Werbung. Alle Funktionen:
IMPORTIERE, ORGANISIERE UND HÖRE
• Importiere oder füge Feeds über das iTunes und gPodder.net Verzeichnis, OMPL Dateien und RSS oder Atom Links hinzu. -• Bediene die Wiedergabe von überall: Homescreen-Widget, Benachrichtigung und Koopfhörer- und Bluetooth-Bedienelementen
+• Bediene die Wiedergabe von überall: Startbildschirm-Widget, Benachrichtigung und Kopfhörer- und Bluetooth-Bedienelemente
• Genieße das Zuhören auf deine Art mit einstellbarer Abspielgeschwindigkeit, der Unterstützung von Kapiteln (MP3, OGG, Podlove) und ausgereifter Schlummerfunktion (durch Schütteln zurücksetzen, Lautstärke verringern und Geschwindigkeit verlangsamen) • Greife auf Passwort-geschützte Feeds und Episoden zu
• Nutze den Vorteil von Paged Feeds (http://www.podlove.org/paged-feeds) @@ -15,9 +15,8 @@ ORDNE, TEILE & GENIEßE • Bleib an den Besten der Besten dran, indem Du Episoden als Favoriten markierst
• Finde Episoden durch die Liste zuletzt gespielter Episoden oder durch Suche in Titel und Shownotes • Teile Episoden and Feeds über soziale Medien, E-Mail, den gPodder.net-Dienst oder als OPML-Export -• Unterstütze die Autoren von Inhalten mit Flattr (inklusive automatischem Flattren) -STEUER DAS SYSTEM
+STEUERE DAS SYSTEM
• Kontrolliere automatisches Herunterladen: Wähle Feeds aus, schließe mobile Netze aus, suche bestimmte WiFi-Netze aus, setze voraus, dass das Smartphone geladen wird und lege Zeitpunkte oder Intervalle fest
• Verwalte deinen Speicherplatz durch das Festlegen der Anzahl gespeicherter Episoden, schlaues Löschen und durch Auswahl des Speicherortes
• Benutze AntennaPod in deiner Sprache (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)
diff --git a/app/src/main/play/listings/de-DE/title.txt b/app/src/main/play/listings/de-DE/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/de-DE/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/en-US/graphics/icon/icon_play.png b/app/src/main/play/listings/en-US/graphics/icon/icon_play.png new file mode 100644 index 000000000..824346d21 Binary files /dev/null and b/app/src/main/play/listings/en-US/graphics/icon/icon_play.png differ diff --git a/app/src/main/play/listings/es-ES/full-description.txt b/app/src/main/play/listings/es-ES/full-description.txt new file mode 100644 index 000000000..8c5cba745 --- /dev/null +++ b/app/src/main/play/listings/es-ES/full-description.txt @@ -0,0 +1,42 @@ +AntennaPod es un reproductor y administrador de pódcast que te da acceso instantáneo a millones de pódcast gratuitos y de pago, desde editores independientes a grandes editoriales como la BBC, NPR y CNN. Añade, importa y exporta tus fuentes sin complicaciones usando la base de datos de pódcast de iTunes, archivos OPML o las URL de tipo RSS. Ahorra esfuerzo, energía de la batería y uso de datos móviles con potentes controles de automatización para descargar episodios (especifica horarios, intervalos y redes wifi) y elimina episodios (según tus favoritos y la configuración de retardo).
+Y lo que es más importante: Descarga, escucha en stream, o añade la cola episodios y disfrútalos como quieras con velocidad de reproducción ajustable, soporte para capítulos y temporizador de sueño. + +Creado por entusiastas del pódcast, AntennaPod es libre en todos los sentidos: código abierto, gratuito y sin publicidad. + +Todas las características:
+IMPORTAR, ORGANIZAR Y REPRODUCIR
+• Añade e importa fuentes mediante los directorios de iTunes y gPodder.net, archivos OPML y enlaces RSS o Atom
+• Administra la reproducción desde cualquier parte: control en pantalla de inicio, notificación del sistema y controles de auricular y bluetooth
+• Disfruta escuchando a tu manera con velocidad de reproducción ajustable, soporte de capítulos (MP3, VorbisComment y Podlove), recordatorio del punto de reproducción y el temporizador de sueño avanzado (agita para restablecer, bajar el volumen y disminuir la velocidad de reproducción)
+• Accede a fuentes y episodios protegidos con contraseña
+• Aprovecha las fuentes paginadas (www.podlove.org/paged-feeds) + +MANTÉN UN SEGUIMIENTO, COMPARTE Y APRECIA +• Haz un seguimiento de lo mejor de lo mejor marcando episodios como favoritos
+• Encuentra ese episodio a través del historial de reproducción o por búsqueda (títulos y notas de episodios)
+• Comparte episodios y fuentes a través de las avanzadas redes sociales y opciones de correo electrónico, los servicios de gPodder.net y la exportación OPML
+ +CONTROLA EL SISTEMA
+• Controla las descargas automáticas: elige las fuentes, excluye las redes móviles, selecciona redes wifi específicas, o solo cuando el teléfono se esté cargando y establece horarios o intervalos
+• Administra el almacenamiento configurando la cantidad de episodios almacenados, el borrado inteligente (según tus favoritos y el estado de reproducción) y selecciona tu ubicación preferida
+• Usa AntennaPod en tu idioma (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)
+• Adáptate a tu entorno usando el tema claro u oscuro
+• Haz una copia de seguridad de tus suscripciones con la integración de gPodder.net y la exportación OPML + +¡Únete a la comunidad AntennaPod!
+AntennaPod está en continuo desarrollo por voluntarios. ¡Tú también puedes contribuir, con tu código o con tus comentarios! + +GitHub es el sitio que debes visitar para solicitar características nuevas, reportar fallos y contribuir con código
+https://www.github.com/AntennaPod/AntennaPod + +Nuestro Grupo de Google es el sitio para compartir tus ideas, momentos favoritos de tus pódcast y tu gratitud a los voluntarios:
+https://groups.google.com/forum/#!forum/antennapod + +¿Tienes una pregunta o quieres darnos tu opinión? +https://twitter.com/@AntennaPod + +Transifex es el sitio para ayudar con las traducciones:
+https://www.transifex.com/antennapod/antennapod + +Echa un vistazo a nuestro programa de pruebas Beta y ser el primero en usar las nuevas características:
+https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/es-ES/short-description.txt b/app/src/main/play/listings/es-ES/short-description.txt new file mode 100644 index 000000000..cc94d9c22 --- /dev/null +++ b/app/src/main/play/listings/es-ES/short-description.txt @@ -0,0 +1 @@ +Reproductor y gestor de pódcast fácil de usar, flexible y de código abierto \ No newline at end of file diff --git a/app/src/main/play/listings/es-ES/title.txt b/app/src/main/play/listings/es-ES/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/es-ES/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/fr-FR/full-description.txt b/app/src/main/play/listings/fr-FR/full-description.txt index 372e1da60..5c69f3892 100644 --- a/app/src/main/play/listings/fr-FR/full-description.txt +++ b/app/src/main/play/listings/fr-FR/full-description.txt @@ -1,5 +1,5 @@ -AntennaPod est un lecteur et gestionnaire de podcast permettant l'accès à des millions de podcast gratuits ou payants produit aussi bien par des podcasters indépendants que de gros éditeurs comme la BBC, NPR ou CNN. Ajoutez, importez et exportez leurs flux facilement à partir d'ITunes, de fichiers OPML ou simplement à partir de liens RSS. Gagnez du temps, préservez votre batterie et consommation internet grâce à une automatisation puissante des téléchargements (date, fréquence, choix du réseau WiFi, etc...) et des suppressions d’épisodes écoutés (selon vos critères)
-Avant tout : téléchargez, streamez ou ajoutez à la liste de lecture vos épisodes et écoutez les comme vous voulez grâce au réglage de vitesse de lecture, au support des chapitres et au minuteur d'arrêt. Vous pouvez même montrer votre appréciation aux créateurs de contenu avec notre intégration de Flattr. +AntennaPod est un lecteur de podcast permettant l'accès à des millions de podcast gratuits ou payants produit aussi bien par des podcasters indépendants que des éditeurs professionnels comme la BBC, NPR ou CNN. Ajoutez, importez et exportez leurs flux facilement à partir d'ITunes, de fichiers OPML ou simplement à partir de liens RSS. Economisez votre temps, votre batterie et votre consommation de données grâce à l'automatisation des téléchargements (date, fréquence, choix du réseau WiFi, etc...) et de la suppression des épisodes (selon vos critères)
+Avant tout : téléchargez, streamez ou ajoutez à la liste de lecture vos épisodes et écoutez les comme vous voulez grâce au réglage de vitesse de lecture, au support des chapitres et au minuteur d'arrêt. Conçu par des fans de podcast, AntennaPod est gratuit dans tous les sens du terme : open source, gratuit et sans publicité. @@ -15,7 +15,6 @@ SUIVEZ, PARTAGEZ & PROFITEZ
• Marquer les meilleurs épisodes en tant que favoris
• Retrouvez un épisode à partir de l'historique de lecture ou en recherchant parmi les titres et commentaires des épisodes précédents
• Partagez vos épisodes et flux sur les réseaux sociaux, par email, sur gPodder.net ou en les exportant au format OPML
-• Soutenez les créateurs de contenu avec l'intégration à Flattr et la possibilité de flatter automatiquement CONTRÔLER LE SYSTÈME
• Prenez le contrôle en automatisant vos téléchargements : choix des flux, restriction de la connexion mobile, sélection du réseau WIFI à utiliser, uniquement durant la recharge et spécifiez la fréquence de mise à jour vous-même
diff --git a/app/src/main/play/listings/fr-FR/short-description.txt b/app/src/main/play/listings/fr-FR/short-description.txt index 61c3c7e20..7143385e1 100644 --- a/app/src/main/play/listings/fr-FR/short-description.txt +++ b/app/src/main/play/listings/fr-FR/short-description.txt @@ -1 +1 @@ -Un lecteur et gestionnaire de podcast facile à utiliser et flexible \ No newline at end of file +Un lecteur de podcast facile à utiliser et flexible \ No newline at end of file diff --git a/app/src/main/play/listings/fr-FR/title.txt b/app/src/main/play/listings/fr-FR/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/fr-FR/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/gl-ES/full-description.txt b/app/src/main/play/listings/gl-ES/full-description.txt index c275c1fc2..2acaf65f7 100644 --- a/app/src/main/play/listings/gl-ES/full-description.txt +++ b/app/src/main/play/listings/gl-ES/full-description.txt @@ -1,5 +1,5 @@ AntenaPod é un xestor de podcast e reprodutor que lle da acceso a millóns de podcast tanto gratuítos como de pagamento, desde podcasters independentes a grandes productores como BBC, NPR e CNN. Engada, importe e exporte as súas fontes de xeito doado utilizando a base de datos de iTunes, ficheiros OPML ou URLs RSS. Aforre traballo, enerxía da batería e datos móbiles co sistema automatizado de control das descargas de episodios (indicando horario, intervalos e redes WiFi) e borrando episodios (baseado nos seus favoritos e axustes de retardo).
-Mais o máis importante: Descargue, envíe ou poña na cola os episodios e disfrúteos do xeito en que máis lle conveña, axustando a velocidade de reprodución, xestión de capítulos e apagado automático. Poderá tamén mostrarlle aos creadores canto lle gustan os seus contidos gracias a integración con Flattr. +O máis importante: Descarga, reproduce ou pon en cola os episodios e desfrútaos do xeito en que máis che conveña axustando a velocidade de reprodución, o soporte de capítulos e o apagado programable. Escrito por namorados dos podcast, AntennaPod é gratuíto e libre: open source, sen custos, sen publicidade. @@ -15,7 +15,6 @@ SIGA, COMPARTA E VALORE
• Garde o melloriño de cada casa marcando episodios como favoritos
• Atope ese episodio especial no historial de reprodución ou buscando (títulos e notas do episodio)
• Comparta episodios e fontes a través das opcións de redes sociais e correro electrónico, os servizos de gPodder.net e exportando a OPML
-• Axude aos creadores de contido coa integración con Flattr incluíndo o flattring automático CONTROL DO SISTEMA
• Tome control sobre as descargas automáticas: escolla fontes, exclúa redes móbiles, redes WIFI concretas, requerir que o móbil esté a cargar e horarios e intervalos
diff --git a/app/src/main/play/listings/gl-ES/title.txt b/app/src/main/play/listings/gl-ES/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/gl-ES/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/it-IT/title.txt b/app/src/main/play/listings/it-IT/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/it-IT/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/.tx/antennapod.description/he_IL_translation b/app/src/main/play/listings/iw-IL/full-description.txt similarity index 93% rename from .tx/antennapod.description/he_IL_translation rename to app/src/main/play/listings/iw-IL/full-description.txt index 4ccee5452..58895c50e 100644 --- a/.tx/antennapod.description/he_IL_translation +++ b/app/src/main/play/listings/iw-IL/full-description.txt @@ -1,5 +1,5 @@ היישומון אנטנה־פּוֹד הוא נגן ומנהל פודקאסטים שמעניק לך גישה ישירה למיליונים של פודקאסטים בחינם ובתשלום, החל ממגישי פודקאסטים עצמאיים ועד למפיצים גדולים כגון BBC,‏ NPR ו־CNN. ניתן להוסיף, לייבא ולייצא את ההזנות שלהם בקלות יחסית באמצעות מסד נתוני הפודקאסטים של iTunes, קובצי OPML או כתובות של RSS. מאפשר לך לחסוך במאמץ, סוללה ותקשורת נתונים עם פקדי אוטומציה להורדה של פרקים (לפי זמנים, הפרשי זמן ורשתות אלחוטיות) ומחיקה של פרקים (על בסיס הגדרות המועדפים וההשהיה שלך).
-אבל הכי חשוב: ניתן להוריד, להזרים או לסדר רשימות של פרקים וליהנות מהם בכל דרך שמתאימה לך עם מהירויות נגינה משתנות, תמיכה במקטעים ומתזמן שינה. ניתן אפילו להביע את חיבתך ליוצרי התוכן עם שילוב של Flattr ביישומון. +אבל הכי חשוב: ניתן להוריד, להזרים או לסדר רשימות של פרקים וליהנות מהם בכל דרך שמתאימה לך עם מהירויות נגינה משתנות, תמיכה במקטעים ומתזמן שינה מיוצרת על ידי חובבי פודקאסטים, אנטנהפוד הינה תוכנה חינמית בכל מובן המילה: קוד פתוח, ללא עלות וללא פרסומות. @@ -15,7 +15,6 @@ • מעקב אחר הטובים שבטובים על ידי סימון פרקים כמועדפים
• ניתן לאתר פרק אחד דרך היסטוריית הנגינה או על ידי חיפוש (כותרות והערות פרק)
• ניתן לשתף פרקים והזנות דרך אפשרויות מתקדמות ברשתות חברתיות ודוא״ל, שירותי gPodder.net ודרך ייצוא OPML
-• ניתן לתמוך בעורכי תוכן עם שילוב של Flattr לתוך המערכת לרבות תרומה אוטומטית שליטה במערכת
• ניתן לשלוט על הורדה אוטומטית: לבחור הזנות, להחריג רשתות סלולריות, לבחור רשתות אלחוטיות מסוימות, לדרוש מהטלפון להיות בטעינה ולהגדיר מועדים או מרווחי זמן
diff --git a/.tx/antennapod.shortdescription/he_IL_translation b/app/src/main/play/listings/iw-IL/short-description.txt similarity index 100% rename from .tx/antennapod.shortdescription/he_IL_translation rename to app/src/main/play/listings/iw-IL/short-description.txt diff --git a/app/src/main/play/listings/iw-IL/title.txt b/app/src/main/play/listings/iw-IL/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/iw-IL/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/ja-JP/full-description.txt b/app/src/main/play/listings/ja-JP/full-description.txt index b85f6787e..4dc50f57e 100644 --- a/app/src/main/play/listings/ja-JP/full-description.txt +++ b/app/src/main/play/listings/ja-JP/full-description.txt @@ -1,5 +1,5 @@ AntennaPodは、独自のポッドキャスターから、BBC、NPR、CNNなどの大規模な放送まで、数百万の無料や有料ポッドキャストに瞬時にアクセスすることができる、ポッドキャストマネージャーおよびプレーヤーです。フィードは手間のかからないiTunesのPodcastのデータベース、OPMLファイルや簡単なRSSのURLを使用して追加、インポート、エクスポートします。エピソードのダウンロード (時間、間隔およびWiFiネットワークを指定) とエピソードの削除 (お気に入りと遅延設定に基づいて) をするために強力な自動コントロールで、手間、バッテリ消費、モバイルデータ使用量を節約します。
-しかし最も重要なこと: エピソードをダウンロード、ストリーム再生、またはキューに入れて、そして再生速度の調整、チャプターのサポート、スリープタイマーで好きなように楽しんでください。Flattr統合でコンテンツ作成者にあなたの愛を示すことができます。 +しかし最も重要なこと: エピソードをダウンロード、ストリーム再生、またはキューに入れて、そして再生速度の調整、チャプターのサポート、スリープタイマーで好きなように楽しんでください。 ポッドキャスト愛好家が作成した AntennaPod はすべての意味でフリー自由です: オープンソース、コスト不要、広告はありません。 @@ -15,7 +15,6 @@ AntennaPodは、独自のポッドキャスターから、BBC、NPR、CNNなど • エピソードをお気に入りとしてマークして、一番の中の一番を保存してください
• 再生履歴から、または検索 (タイトルとショーノート) して目的のエピソードを見つけてください
• 高度なソーシャルメディアとメールオプション、gPodder.netサービス、OPMLエクスポートからエピソードやフィードを共有してください
-• 自動Flattrを含むFlattrの統合でコンテンツクリエイターをサポートします システムのコントロール • 自動ダウンロードの制御: フィードを選択、モバイルネットワークを除外、特定のWiFiネットワークを選択、電話を充電する必要、時間や間隔を設定
diff --git a/app/src/main/play/listings/ja-JP/title.txt b/app/src/main/play/listings/ja-JP/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/ja-JP/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/nl-NL/full-description.txt b/app/src/main/play/listings/nl-NL/full-description.txt index 6c6ef1e8a..518745f17 100644 --- a/app/src/main/play/listings/nl-NL/full-description.txt +++ b/app/src/main/play/listings/nl-NL/full-description.txt @@ -1,5 +1,5 @@ Met AntennaPod speel en beheer je al je podcasts en krijg je directe toegang tot duizenden gratis en betaalde podcasts - van onafhankelijke makers tot grote merken zoals BBC, CNN en NPO. Via de iTunes-databank, OPML-bestanden en simpele RSS-linkjes voeg je deze podcasts gemakkelijk toe. Dankzij simpele maar slimme automatische controle van het downloaden en verwijderen van afleveringen bespaar je de accu, hoef je je favoriete podcast niet meer handmatig te volgen en verbruik je geen onnodige mobiele gegevens.
-Maar belangrijker: Met een handige wachtrij, aanpasbare afspeelsnelheden, ondersteuning van hoofdstukken en een slaap timer luister je naar podcasts op de manier die jij prettig vindt. Je kunt zelfs je liefde voor de makers uiten met onze Flattr-integratie. +Maar belangrijker: download, stream of voeg afleveringen toe aan de wachtrij en geniet ervan! Je hebt beschikking over afspeelsnelheden, hoofdstukondersteuning en een slaaptimer. Gemaakt door podcast-enthousiastelingen, AntennaPod is vrij in de breedste zin van het woord: vrij van advertenties, open source en gratis. @@ -15,7 +15,6 @@ DEEL & WAARDEER
• Hou het beste van het beste bij door afleveringen als favoriet te markeren
• Vindt die ene aflevering terug in de afspeelgeschiedenis of door te zoeken (in titels, shownotes en makers)
• Deel podcasts en afleveringen via uitgebreide opties voor sociale media, WhatsApp, email en gPodder.net
-• Steun makers met Flattr-integratie, inclusief auto-Flattr HOUD DE CONTROLE • Beheer automatische downloads: kies je podcasts, sluit mobiele netwerken uit, selecteer specifieke WiFi-verbindingen, eis dat de telefoon wordt opgeladen and bepaal tijden of intervals
diff --git a/app/src/main/play/listings/nl-NL/title.txt b/app/src/main/play/listings/nl-NL/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/nl-NL/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/sv-SE/full-description.txt b/app/src/main/play/listings/sv-SE/full-description.txt new file mode 100644 index 000000000..d46fdafdc --- /dev/null +++ b/app/src/main/play/listings/sv-SE/full-description.txt @@ -0,0 +1,42 @@ +AntennaPod är en podcasthanterare och spelare som ger dig omedelbar tillgång till miljoner av gratis och betalda podcasts, från oberoende podcastare till stora publiceringshus så som BBC, NPR och CNN. Lägg till, importera och exportera enkelt deras flöden med iTunes podcastdatabas, OPML-filer eller vanliga RSS URL:er. Spara möda, batterikraft och mobildata med kraftfulla automatiseringskontroller för nedladdning (välj tider, intervall och WiFi-nätverk) och borttagning av episoder (baserat på dina favoriter och fördröjningsinställningar). +Men viktigast av allt: Ladda ner, strömma eller köa episoder och avnjut dem på det sätt du gillar med justerbar uppspelningshastighet, kapitelstöd och insomningstimer. + +Gjord av podcastenthusiaster, AntennaPod är fri i alla ordets bemärkelser: öppen källkod, inga kostnader, ingen reklam. + +Alla funktioner:
+IMPORTERA, ORGANISERA OCH SPELA
+• Lägg till och importera flöden via iTunes och gPodder.net, OPML filer och RSS eller Atom länkar
+• Hantera uppspelningen från vartsomhelst: hemskärmswidget, aviseringsfältet och hörlurs/bluetoth-kontroller
+• Njut av att lyssna på ditt sätt med justerbar uppspelningshastighet, kapitelstöd (MP4, VorbisComment och Podlove), ihågkommen uppspelningsposition och en avancerad sömntimer (skaka för återställaning, sänk volymen och sänk hastigheten)
+• Kom åt lösenordsskyddade flöden och episoder
+• Dra nytta av siduppdelade flöden (www.podlove.ord/paged-feeds) + +SPÅRA, DELA & UPPSKATTA
+• Håll ordning på de bästa av de bästa med favoritmarkering av episoder
+• Hitta just den där episoden i uppspelningshistoriken eller genom sökning (titel och shownotes)
+• Dela episoder och flöden med avancerade vald för social media och och email, tjänsten gPodder.net och via OPML export
+ +KONTROLLERA SYSTEMET
+• Ta kontroll över automatisk nedladdning: välj flöden, exkludera mobilnätverk, välj specifika WiFi nätverk, kräv att telefonen är inkopplad för laddning och sätt tider eller intervall för körning
+• Hantera lagringsutrymme genom att välja antalet cachade episoder, smart borttagning (baserat på dina favoriter och uppspelningsstatus) och välj den lagringsplats du föredrar
+• Använd AntennaPod på ditt språk (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)
+• Anpassa till din omgivning med det ljusa och mörka temat
+• Ta backup av dina prenumerationer med integreringen av gPodder.net och OPML exportering + +Gå med i AntennaPods gemenskap!
+AntennaPod är under aktiv utveckling av volontärer. Du kan också bidra, med kod eller kommentarer! + +GitHub är platsen att gå till för att be om funktioner, skapa buggrapporter eller bidra med kod:
+https://www.github.com/AntennaPod/AntennaPod + +Vår Google Group är platsen för att dela idéer, dina favoritögonblick med podcasting och din uppskattning till volontärerna:
+https://groups.google.com/forum/#!forum/antennapod + +Har du frågor eller vill ge feedback? +https://twitter.com/@AntennaPod + +Transifex är platsen att gå till för att hjälpa till med översättningen:
+https://www.transifex.com/antennapod/antennapod + +Kolla in vårat Beta Testing program för att få de senaste funktionerna först:
+https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/sv-SE/short-description.txt b/app/src/main/play/listings/sv-SE/short-description.txt new file mode 100644 index 000000000..2273fc5e4 --- /dev/null +++ b/app/src/main/play/listings/sv-SE/short-description.txt @@ -0,0 +1 @@ +Användarvänlig och flexibel podcasthanterare och spelare med öppen källkod \ No newline at end of file diff --git a/app/src/main/play/listings/sv-SE/title.txt b/app/src/main/play/listings/sv-SE/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/sv-SE/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/zh-CN/full-description.txt b/app/src/main/play/listings/zh-CN/full-description.txt new file mode 100644 index 000000000..a39b43eee --- /dev/null +++ b/app/src/main/play/listings/zh-CN/full-description.txt @@ -0,0 +1,42 @@ +AntennaPod 是一款播客管理器和播放器,可让您即时收听数百万免费和付费的播客,从独立播客到大型出版商如BBC,NPR和CNN;使用 iTunes 播客数据库,OPML文件或简单的RSS URL轻松添加,导入和导出 feeds ;通过强大的自动化控制功能节省工作量,电池电量和移动数据使用情况,以便下载剧集(指定时间,间隔和 WiFi 网络)和删除剧集(根据您的收藏和延迟设置)。 +但最重要的是:下载,串流或安排节目并用可调节回放速度,章节支持和睡眠定时器以您喜欢的方式享受它们。 + +AntennaPod由播客爱好者开发,在任意方面上都是自由的:开源,免费,无广告。 + +所有功能:
+导入、整理与播放
+• 通过 iTunes、gPodder.net、OPML 文件、RSS 或 Atom 链接添加和导入订阅 +•;从任意地点管理回放:主屏widget,系统通知,耳塞和蓝牙控制装置 +•;借助可调节回放速度,章节支持(Mp3、Vorbis注释和Podlove),记忆的回放位置和一个高级的睡眠定时器 (摇动设备来重置、调低音量并减慢回放)以您自己的方式享受收听播客节目 +#8226; 访问受密码保护的 feeds 和剧集 +• 充分利用分页 feeds (www.podlove.org/paged-feeds) + +保持更新,分享&欣赏 +&#8226; 通过将剧集标记为收藏来追踪最佳的剧集 +• 通过回放历史或搜索(标题和节目笔记)找到您需要的那期节目 +• 通过高级社交媒体和电邮选项,gPodder.net服务和导出OPML文件来分享节目和源 + +一切尽在掌控
+• 控制自动下载:选择源,排除移动网络,选择特定无线网络,要求手机充电并设定次数或间隔 +• 通过设定缓存节目的数量管理存储,智能删除(基于您的喜好和播放状态)并选择您偏好的存储位置 +• 使用对应您的语言的 AntennaPod (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)
+&#8226; 使用浅色和深色主题以适应您的环境 +&#8226; 使用 gPodder.net 集成功能和 OPML 导出以备份您的订阅 + +加入 AntennaPod 社区!
+志愿者正在积极开发 AntennaPod 。您也可以通过代码或评论做出贡献! + +GitHub 是申请增加功能,报告错误和贡献代码的地方: +https://www.github.com/AntennaPod/AntennaPod + +对于所有志愿者来说,我们的谷歌社群是分享你的创意,收藏的播客瞬间和表达感激的地方: +https://groups.google.com/forum/#!forum/antennapod + +抱有疑问或者想要向我们提供反馈? +https://twitter.com/@AntennaPod + +Transifex 是与翻译者帮助项目的网站: +https://www.transifex.com/antennapod/antennapod + +查看我们的测试程序以获得最新的功能列表: +https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod \ No newline at end of file diff --git a/app/src/main/play/listings/zh-CN/short-description.txt b/app/src/main/play/listings/zh-CN/short-description.txt index ab28b4df7..e6072fcf3 100644 --- a/app/src/main/play/listings/zh-CN/short-description.txt +++ b/app/src/main/play/listings/zh-CN/short-description.txt @@ -1 +1 @@ -简单易用、灵活的开源播客管理工具与播放器 \ No newline at end of file +易用、灵活的开源播客管理工具与播放器 \ No newline at end of file diff --git a/app/src/main/play/listings/zh-CN/title.txt b/app/src/main/play/listings/zh-CN/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/zh-CN/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 7c1b36539..11371852d 100644 --- a/app/src/main/play/release-notes/en-US/default.txt +++ b/app/src/main/play/release-notes/en-US/default.txt @@ -4,4 +4,5 @@ - 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) -- Tons of bug fixes and performance improvements (by @andersonvom, @archibishop, @ByteHamster, @gaul, @jas14) +- Added filter to "All episodes" list (by @jhunnius) +- Tons of bug fixes and performance improvements diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index 011aa4c8b..a7f7d9f12 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -10,6 +10,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" + android:focusableInTouchMode="true" android:padding="8dp"> @@ -39,8 +42,10 @@ android:inputType="text" android:imeOptions="actionSearch" android:importantForAutofill="no" - android:layout_marginLeft="8dp" + android:layout_marginStart="0dp" + android:layout_marginLeft="0dp" android:layout_marginRight="8dp" + android:layout_marginEnd="8dp" android:paddingTop="16dp" android:paddingBottom="16dp" android:hint="@string/search_podcast_hint" @@ -109,16 +114,17 @@ + android:layout_marginBottom="4dp"/> + android:layout_marginBottom="4dp"/> + - + @@ -21,12 +39,13 @@ android:id="@+id/progLoading" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" + android:gravity="center" android:indeterminateOnly="true" android:visibility="gone" + android:layout_centerInParent="true" tools:visibility="gone" tools:layout_width="match_parent" tools:layout_height="64dp" tools:background="@android:color/holo_red_light"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/bug_report.xml b/app/src/main/res/layout/bug_report.xml new file mode 100644 index 000000000..e97e85265 --- /dev/null +++ b/app/src/main/res/layout/bug_report.xml @@ -0,0 +1,28 @@ + + +