diff --git a/.tx/config b/.tx/config index b1a6121d6..b5e2efc75 100644 --- a/.tx/config +++ b/.tx/config @@ -15,6 +15,7 @@ trans.fr = res/values-fr/strings.xml trans.hi_IN = res/values-hi-rIN/strings.xml trans.it_IT = res/values-it-rIT/strings.xml trans.nl = res/values-nl/strings.xml +trans.pl_PL = res/values-pl-rPL/strings.xml trans.pt = res/values-pt/strings.xml trans.pt_BR = res/values-pt-rBR/strings.xml trans.ro_RO = res/values-ro-rRO/strings.xml diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cf3f1e7b3..979edf4ce 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,15 +1,16 @@ + android:versionCode="34" + android:versionName="0.9.8.1" > + + android:targetSdkVersion="19" /> @@ -153,7 +154,7 @@ android:name=".service.download.DownloadService" android:enabled="true"/> @@ -194,7 +195,7 @@ @@ -327,7 +328,7 @@ android:configChanges="keyboardHidden|orientation"> + android:value="de.danoeh.antennapod.activity.MiroGuideMainActivity" /> Logo -

AntennaPod, Version 0.9.8.0

+

AntennaPod, Version 0.9.8.1

Copyright © 2012 Daniel Oeh

diff --git a/build.gradle b/build.gradle index 20166fa52..33d597340 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.6.+' + classpath 'com.android.tools.build:gradle:0.8.+' } } apply plugin: 'android' @@ -23,9 +23,9 @@ dependencies { new URL('http://www.aocate.com/presto/client/presto_client-0.8.5.jar').withInputStream{ i -> prestoLib.withOutputStream{ it << i }} } - compile 'com.android.support:appcompat-v7:18.0.+' - compile 'org.apache.commons:commons-lang3:3.1' - compile ('org.shredzone.flattr4j:flattr4j-core:2.7') { + compile 'com.android.support:appcompat-v7:19.0.+' + compile 'org.apache.commons:commons-lang3:3.2.1' + compile ('org.shredzone.flattr4j:flattr4j-core:2.8') { exclude group: 'org.apache.httpcomponents', module: 'httpcore' exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.json', module: 'json' @@ -37,12 +37,12 @@ dependencies { } android { - compileSdkVersion 18 - buildToolsVersion "18.1.0" + compileSdkVersion 19 + buildToolsVersion "19.0.1" defaultConfig { minSdkVersion 10 - targetSdkVersion 18 + targetSdkVersion 19 testPackageName "de.test.antennapod" testInstrumentationRunner "instrumentationTest.de.test.antennapod.AntennaPodTestRunner" } @@ -94,4 +94,9 @@ android { signingConfig signingConfigs.releaseConfig } } + + packagingOptions { + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE.txt' + } } diff --git a/pom.xml b/pom.xml index a1fa49244..d70f2efc8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.danoeh antennapod apk - 0.9.8.0 + 0.9.8.1 AntennaPod @@ -13,18 +13,18 @@ android.support compatibility-v4 - 18 + 19 android.support compatibility-v7-appcompat - 18 + 19 apklib android.support compatibility-v7-appcompat - 18 + 19 jar @@ -58,15 +58,16 @@ - com.google.android + android android provided - 4.1.1.4 + 4.4_r1 - com.google.android + com.google.android.annotations annotations - 4.1.1.4 + 22.3 + provided commons-io @@ -109,11 +110,11 @@ com.jayway.maven.plugins.android.generation2 android-maven-plugin - 3.6.1 + 3.8.0 ${env.ANDROID_HOME} - 18 + 19 true diff --git a/project.properties b/project.properties index 75f295e31..57e7d75fe 100644 --- a/project.properties +++ b/project.properties @@ -9,7 +9,6 @@ # Project target. proguard.config=proguard.cfg -target=android-18 -android.library.reference.1=submodules/ActionBarSherlock/library -android.library.reference.2=submodules/ViewPagerIndicator/library -android.library.reference.3=submodules/dslv/library +target=android-19 +android.library.reference.1=submodules/dslv/library + diff --git a/res/drawable-hdpi/ic_action_pause_over_video.png b/res/drawable-hdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..64b07728f Binary files /dev/null and b/res/drawable-hdpi/ic_action_pause_over_video.png differ diff --git a/res/drawable-hdpi/ic_action_play_over_video.png b/res/drawable-hdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..a364ca7c2 Binary files /dev/null and b/res/drawable-hdpi/ic_action_play_over_video.png differ diff --git a/res/drawable-mdpi/ic_action_pause_over_video.png b/res/drawable-mdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..f478ac321 Binary files /dev/null and b/res/drawable-mdpi/ic_action_pause_over_video.png differ diff --git a/res/drawable-mdpi/ic_action_play_over_video.png b/res/drawable-mdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..835ff3636 Binary files /dev/null and b/res/drawable-mdpi/ic_action_play_over_video.png differ diff --git a/res/drawable-xhdpi/ic_action_pause_over_video.png b/res/drawable-xhdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..b0777a023 Binary files /dev/null and b/res/drawable-xhdpi/ic_action_pause_over_video.png differ diff --git a/res/drawable-xhdpi/ic_action_play_over_video.png b/res/drawable-xhdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..24331a48c Binary files /dev/null and b/res/drawable-xhdpi/ic_action_play_over_video.png differ diff --git a/res/drawable-xxhdpi/ic_action_pause_over_video.png b/res/drawable-xxhdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..fa85601cf Binary files /dev/null and b/res/drawable-xxhdpi/ic_action_pause_over_video.png differ diff --git a/res/drawable-xxhdpi/ic_action_play_over_video.png b/res/drawable-xxhdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..121be211e Binary files /dev/null and b/res/drawable-xxhdpi/ic_action_play_over_video.png differ diff --git a/res/drawable/overlay_button_circle_background.xml b/res/drawable/overlay_button_circle_background.xml new file mode 100644 index 000000000..90c51472c --- /dev/null +++ b/res/drawable/overlay_button_circle_background.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/res/layout-land/audioplayer_activity.xml b/res/layout-land/audioplayer_activity.xml index 1e671c745..7900e1ced 100644 --- a/res/layout-land/audioplayer_activity.xml +++ b/res/layout-land/audioplayer_activity.xml @@ -26,6 +26,7 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"/> - + android:indeterminateOnly="true"/> + + - - - - - - - - - + android:background="#80000000" + android:orientation="vertical"> + android:layout_height="50dp" + android:paddingTop="8dp" + android:layout_marginBottom="4dp"> + android:layout_marginTop="4dp" + android:textColor="@color/white" + android:textStyle="bold" + android:text="@string/position_default_label"/> + android:layout_marginTop="4dp" + android:textColor="@color/white" + android:textStyle="bold" + android:text="@string/position_default_label"/> + android:max="500"/> diff --git a/res/layout-v14/directory_chooser.xml b/res/layout-v14/directory_chooser.xml index 3bb1d5c9c..f0bef72e4 100644 --- a/res/layout-v14/directory_chooser.xml +++ b/res/layout-v14/directory_chooser.xml @@ -56,6 +56,7 @@ @@ -31,7 +32,8 @@ android:layout_width="@dimen/enc_icons_size" android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvPublished" - android:padding="2dp" /> + android:padding="2dp" + tools:ignore="ContentDescription"/> + android:layout_marginRight="8dp" + tools:ignore="ContentDescription"/> diff --git a/res/layout/feedinfo.xml b/res/layout/feedinfo.xml index e1c1c10a3..d975ef549 100644 --- a/res/layout/feedinfo.xml +++ b/res/layout/feedinfo.xml @@ -12,6 +12,7 @@ - - - - + + + + + diff --git a/res/layout/feeditemlist_header.xml b/res/layout/feeditemlist_header.xml index 31ccb7e96..560013abd 100644 --- a/res/layout/feeditemlist_header.xml +++ b/res/layout/feeditemlist_header.xml @@ -27,6 +27,7 @@ @@ -45,10 +46,12 @@ android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvPublished" android:layout_toLeftOf="@+id/imgvInPlaylist" - android:padding="2dp" /> + android:padding="2dp" + tools:ignore="ContentDescription"/> + android:src="@drawable/white_circle" + tools:ignore="ContentDescription"/> + android:src="@drawable/av_play_dark" + tools:ignore="ContentDescription"/> diff --git a/res/layout/feedlist_item_grid.xml b/res/layout/feedlist_item_grid.xml index c761a3a84..934904374 100644 --- a/res/layout/feedlist_item_grid.xml +++ b/res/layout/feedlist_item_grid.xml @@ -1,10 +1,12 @@ + android:src="@drawable/white_circle" + tools:ignore="ContentDescription"/> + android:src="@drawable/av_play_dark" + tools:ignore="ContentDescription"/> diff --git a/res/layout/gpodnet_podcast_listitem.xml b/res/layout/gpodnet_podcast_listitem.xml index f6ddb3bd8..1f6cdd1d0 100644 --- a/res/layout/gpodnet_podcast_listitem.xml +++ b/res/layout/gpodnet_podcast_listitem.xml @@ -5,6 +5,7 @@ android:layout_height="match_parent"> Müəlif Dil Parametrlər - Üz: Xəta Xəta baş verdi: Təzələ @@ -128,8 +127,7 @@ Bu əməliyyat üçün AntennaPod\'un icazəsi yoxdur. AntennaPod\'un keçid tokenin ləğv olunması bunun səbəbi ola bilər. Yenə Flattra girin ya da podkastın səhifəsinə keçin. Keçid ləğv olundu AntennaPod\'un keçid tokeni uğurlu ləğv olundu. - Flattrma uğurludur - Flattrləmə + Plagin yüklə Plagin yüklü deyil @@ -235,4 +233,5 @@ Başlanğıc qovluqu seç Yükləmə... + diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 243b63772..94661466d 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -28,7 +28,7 @@ Autor Llengua Configuració - Caràtula + Imatge Error S\'ha produït un error: Actualitza @@ -140,8 +140,7 @@ AntennaPod no té permisos per executar aquesta acció. És possible que el testimoni d\'accés de Flattr per a AntennaPod hagi estat revocat. Podeu tornar-vos a autenticar amb el servei de Flattr, o podeu visitar el web del contingut directament. L\'accés ha estat revocat El testimoni d\'accés a Flattr de l\'AntennaPod s\'ha revocat correctament. Per completar el procés, heu de suprimir aquesta aplicació de la llista d\'aplicacions aprovades que trobareu a l\'apartat de configuració del compte de la plana web de Flattr. - S\'ha compartit el contingut a través de Flattr - S\'està compartint amb Flattr + Baixa el connector Connector no instal·lat @@ -292,4 +291,5 @@ Subscriu Subscrit S\'està baixant... + diff --git a/res/values-cs-rCZ/strings.xml b/res/values-cs-rCZ/strings.xml index 9d7881057..7958175bd 100644 --- a/res/values-cs-rCZ/strings.xml +++ b/res/values-cs-rCZ/strings.xml @@ -28,7 +28,6 @@ Autor Jazyk Nastavení - Obal Chyba Nastala chyba: Obnovit @@ -140,8 +139,7 @@ AntennaPod nemá oprávnění pro tuto akci. Důvodem může být revokování přístupového tokenu AntennaPodu k vašemu účtu. Přístup můžete obnovit nebo využít prohlížeče k návštěvě stránky zdroje. Přístup revokován Úspěšně revokován přístup AntennPodu k vašemu účtu. Pro dokončení tohoto procesu je ještě zapotřebí na stránkách flattru odebrat z vašeho účtu AntennaPod ze seznamu povolených aplikací. - Úspěšně flattrováno! - Flattruji + Stáhnout Plugin Plugin není nainstalován @@ -292,4 +290,5 @@ Odebírat Odebíráno Stahuji... + diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index 116ece995..a3e770f3d 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -24,7 +24,6 @@ Annuller Forfatter Sprog - Cover Fejl En fejl er opstået: Opdater @@ -126,8 +125,7 @@ AntennaPod har ikke tilladelse til denne handling. Årsagen kunne være at adgangspoletten for AntennaPod til din konto er blevet tilbagekaldt. Du kan enten godkende den igen eller besøge websiden for mediet istedet. Adgang tilbagekaldt Du har succesfuldt tilbagekaldt AntennaPods adgangs polet til din konto. For at fuldføre processen skal du fjerne denne app fra listen af godkendte applikationer i din kontos indstillinger på flattr\'s hjemmeside. - Det er lykkedes at flattr dette emne! - Flattere + Hent Plugin Plugin er ikke installeret @@ -230,4 +228,5 @@ Mappen du har valgt er ikke tom. Medie downloads og andre filer vil blive placeret i denne mappe. Forsæt alligevel? Vælg standard mappe + diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index e25b60fe4..8f1aabcff 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -28,7 +28,7 @@ Autor Sprache Einstellungen - Cover + Bild Fehler Ein Fehler ist aufgetreten: Aktualisieren @@ -140,8 +140,18 @@ AntennaPod besitzt keine Erlaubnis für diese Aktion. Der Grund dafür könnte sein, dass AntennaPods Zugangstoken aufgehoben worden ist. Du kannst dich entweder erneut authentifizieren oder die Flattr-Seite der Sache im Web besuchen. Zugriff widerrufen Du hast AntennaPod das Zugangstoken zu deinem Account entzogen. Um diesen Prozess abzuschließen, musst du diese Anwendung aus der Liste der zugelassenen Anwendungen in deinen Account Einstellungen auf der Flattr Webseite entfernen. - Du hast erfolgreich diese Sache mit Flattr unterstützt! - Flattre diese Sache + + Eine Sache wurde geflattrt! + %d Sachen wurden geflattrt! + Geflattrt: %s + Flattrn von %d Sachen fehlgeschlagen! + Nicht geflattrt: %s + Sache wird später gelfattrt + Flattrt: %s + AntennaPod flattrt + AntennaPod hat geflattrt + AntennaPod Flattrn fehlgeschlagen + Rufe geflatterte Sachen ab Plugin herunterladen Plugin nicht installiert @@ -176,6 +186,8 @@ Unterstütze die Entwicklung von AntennaPod mit Flattr. Danke! Zugriff entziehen Entziehe dieser Anwendung die Zugriffserlaubnis für deinen Flattr Account. + Automatisches Flattrn + Flattr Episoden, die zu 80 Prozent gespielt wurden. Nur Episoden anzeigen Zeige nur Feed-Einträge mit Episoden an. Benutzeroberfläche @@ -292,4 +304,21 @@ Abonnieren Abonniert Lade herunter... + + Kapitel anzeigen + Sendungsnotizen anzeigen + Bild anzeigen + Zurückspulen + Vorspulen + Audio + Video + Nach oben navigieren + Mehr Aktionen + Episode wird gerade abgespielt + Episode wird gerade heruntergeladen + Episode ist heruntergeladen + Eintrag ist neu + Episode befindet sich inder Abspielliste + Anzahl neuer Episoden + Anzahl der Episoden, die du angefangen hast zu hören diff --git a/res/values-es-rES/strings.xml b/res/values-es-rES/strings.xml index 541019d0b..721faa65e 100644 --- a/res/values-es-rES/strings.xml +++ b/res/values-es-rES/strings.xml @@ -24,7 +24,6 @@ Cancelar Autor Idioma - Carátula Error Ha ocurrido un error: Actualizar @@ -122,8 +121,7 @@ AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa. Acceso revocado Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr. - Ha valorado esto en Flattr. - Valoración en Flattr + Esta lista no tiene elementos. @@ -218,4 +216,5 @@ La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente? Elegir carpeta predeterminada + diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index c806fdea8..4d8dbd85b 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -28,7 +28,7 @@ Autor Idioma Ajustes - Carátula + Imagen Error Ha ocurrido un error: Actualizar @@ -140,8 +140,18 @@ AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa. Acceso revocado Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr. - Ha valorado esto en Flattr. - Valoración en Flattr + + ¡Flattr una cosa! + ¡Flattr %d cosas! + Flattr: %s. + ¡Falló Flattr de %d cosas! + No se hizo Flattr: %s. + Se hará Flattr de esta cosa más tarde + Haciendo Flattr de %s + AntennaPod haciendo Flattr + AntennaPod hizo Flattr + AntennaPod Flattr falló + Obteniendo lista de Flattr Descargar complemento Complemento no instalado @@ -176,6 +186,8 @@ Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias! Revocar el acceso Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr. + Uso de Flattr automático + Hacer Flattr al reproducir el 80 por ciento de cada episodio Mostrar solo episodios Mostrar solo elementos que contengan un episodio. Interfaz de usuario @@ -292,4 +304,21 @@ Suscribirse Suscrito Descargando… + + Mostrar capítulos + Mostrar notas del programa + Mostrar imagen + Rebobinar + Avance rápido + Audio + Vídeo + Navegar hacia arriba + Más acciones + El episodio se está reproduciendo + El episodio se está descargando + El episodio está descargado + El elemento es nuevo + El episodio está en la cola + Cantidad de episodios nuevos + Cantidad de episodios que ha comenzado a escuchar diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index c8bd652f3..8a8492b67 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -28,7 +28,7 @@ Auteur Langue Préférences - Couverture + Image Erreur Une erreur a eu lieu : Rafraîchir @@ -140,8 +140,18 @@ AntennaPod n\'a pas la permission pour cette action. Il est possible que l\'accès à votre compte depuis AntennaPod ait été révoqué. Vous pouvez vous authentifier à nouveau, ou bien visiter le site à flattr directement. Accès révoqué Vous avez révoqué le jeton d\'accès d\'AntennaPod à votre compte. Pour terminer cette opération, vous devez retirer AntennaPod de la liste des applications autorisées sur le site web de Flattr. - Flattr réussi ! - Flattr en cours + + Une chose de Flattré ! + %d choses de Flattré ! + Flattré : %s. + Impossible de Flattrer %d choses ! + Non Flattré : %s. + Cette chose sera Flattré plus tard + En train de Flattrer %s + AntennaPod est en train de Flattrer + AntennaPod a Flattré + Flattr d\'AntennaPod a échoué + Obtention de la liste des choses Flattrées Télécharger une extension Extension non installée @@ -176,6 +186,8 @@ Encouragez le développement d\'AntennaPod grâce à Flattr. Merci ! Révoquer l\'accès Révoquer la permission d\'accès à votre compte Flattr depuis cette application. + Flattr automatique + Flattrer les épisodes dont 80 pour-cents ont été joués. N\'afficher que les épisodes N\'afficher que les flux qui ont au moins un épisode. Interface utilisateur @@ -292,4 +304,21 @@ S\'abonner Abonné Téléchargement en cours + + Afficher chapitres + Afficher notes d\'épisode + Afficher image + Retour en arrière + Avance rapide + Audio + Vidéo + Naviguer vers le haut + Plus d\'actions + L\'épisode est en train d\'être joué + L\'épisode est en train d\'être téléchargé + L\'épisode a été téléchargé + L\'élément est nouveau + L\'épisode est dans la liste + Nombre de nouveaux épisodes + Nombre d\'épisodes que vous avez commencé à écouter diff --git a/res/values-hi-rIN/strings.xml b/res/values-hi-rIN/strings.xml index 0ce8dce4c..faaed3dcd 100644 --- a/res/values-hi-rIN/strings.xml +++ b/res/values-hi-rIN/strings.xml @@ -28,7 +28,6 @@ \tनिर्माता भाषा सेटिंग्स - आवरण त्रुटि एक त्रुटि हो गई: ताज़ा करें @@ -140,8 +139,7 @@ AntennaPod को इस कार्रवाई के लिए अनुमति नहीं है.इस के लिए कारण हो सकता है की आपके खाते में AntennaPod की पहुँच टोकन को निरस्त किया गया है.आप या तो फिर से प्रमाणित कर सकते हैं या बजाय किसी बात के वेबसाइट पर जा सकते हैं. प्रवेश निरस्त किया आपने सफलतापूर्वक अपने खाते में AntennaPod पहुँच टोकन निरस्त कर दिया है. इस प्रक्रिया को पूरा करने के लिए, आपको flattr वेबसाइट पर अपने खाते की सेटिंग्स में अनुमोदित आवेदनों की सूची से इस एप्लिकेशन को हटाना होगा. - सफलतापूर्वक इस बात flattred कर दिया गया है! - Flattr करें + प्लगइन डाउनलोड करें प्लगइन स्थापित नहीं हुआ @@ -292,4 +290,5 @@ सदस्यता लें सदस्यता ली गई डाउनलोड कर रहा है ... + diff --git a/res/values-it-rIT/strings.xml b/res/values-it-rIT/strings.xml index aae39d327..cd71af90c 100644 --- a/res/values-it-rIT/strings.xml +++ b/res/values-it-rIT/strings.xml @@ -28,7 +28,7 @@ Autore Lingua Impostazioni - Copertina + Immagine Errore Un errore è stato rilevato: Aggiorna @@ -140,8 +140,8 @@ AntennaPod non ha il permesso di effettuare questa azione. La ragione potrebbe essere che il token di accesso di AntennaPod al tuo account è stato revocato. Puoi eseguire la re-autenticazione o altrimenti visitare il sito web. Accesso revocato Hai revocato l\'accesso di AntennaPod al tuo account. Al fine di completare il processo devi rimuovere l\'app dalla lista delle applicazioni autorizzare nelle impostazioni del tuo account sul sito di flattr. - Flattr eseguito con successo! - Flattr in corso + + AntennaPod sta eseguendo Flattr Scarica Plugin Plugin non installato @@ -292,4 +292,20 @@ Abbonati Abbonato Download in corso... + + Mostra i capitoli + Mostra le note dell\'episodio + Mosta l\'immagine + Riavvolgi + Avanti veloce + Audio + Video + Naviga su + Più azioni + L\'episodio è in corso di ripoduzione + L\'episodio sta per essere scaricato + L\'episodio è stato scaricato + L\'oggetto è nuovo + L\'episodio è in coda + Numero dei nuovi episodi diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 6f26c5d1f..68537d1d8 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -8,10 +8,13 @@ Nieuw Wachtlijst Instellingen + Podcast toevoegen Downloads Annuleer download Download log Afspeelgeschiedenis + gpodder.net + gpodder.net login In de browser openen URL kopieren @@ -24,13 +27,15 @@ Annuleer Auteur Taal - Cover + Instellingen + Beeld Fout Er is een fout opgetreden: Verversen Geen externe opslag beschikbaar. Zorg ervoor dat de externe opslag gemonteerd is, zodat de app goed kan werken. Hoofdstukken Shownotes + Beschrijving Meest recent episode:\u0020 \u0020episodes Gepubliceerd:\u0020 @@ -39,9 +44,14 @@ Aan het verwerken Laden... Beeld van:\u0020 + Gebruikersnaam en wachtwoord opslaan Sluiten + Opnieuw proberen + Voor het automatisch downloaden beschouwen Feed URL + Podcast toevoegen bij URL + Podcast lijsten Alles als gelezen markeren Toon informatie @@ -49,6 +59,7 @@ Website link delen Feed link delen Bevestig dat u deze feed en ALLE episodes van deze feed die u hebt gedownload wilt verwijderen. + Feed verwijderen Download Spelen @@ -84,6 +95,7 @@ Misvormde URL IO fout Fout in de aanvraag + Databasetoegangsfout Nog \u0020 downloads Podcast gegevens aan het downloaden %1$d downloads geslaagd, %2$d mislukt @@ -113,6 +125,8 @@ Wachtrij organiseren Ongedaan maken Item verwijderd + Naar boven verplaatsen + Naar beneden verplaatsen Flattr inloggen Druk op onderstaande knop om het verificatieproces te starten. U wordt doorgestuurd naar de Flattr inlogscherm in uw browser en wordt gevraagd om toestemming aan AntennaPod te geven om dingen te Flattr\'en. Nadat u toestemming hebt gegeven, keert u automatisch terug naar dit scherm. @@ -126,8 +140,7 @@ AntennaPod heeft geen toestemming voor deze actie. De reden hiervoor zou kunnen zijn dat de toegang token van AntennaPod voor uw account ingetrokken is. U kunt opnieuw authenticeren, of de website van het ding bezoeken. Toegang ingetrokken U heeft met succes het toegangstoken van AntennaPod tot uw account ingetrokken. Om het proces te voltooien, moet u deze app uit de lijst van goedgekeurde applicaties in uw accountinstellingen op de Flattr website verwijderen. - Dit ding met succes geflattr\'d! - Aan het flattr\'en + Plugin downloaden Plugin niet geinstalleerd @@ -140,6 +153,8 @@ Overig Over AntennaPod Wachtrij + Services + Flattr Afspelen pauzeren wanneer de hoofdtelefoon wordt losgekoppeld Volgende wachtrij item afspelen als de episode voltooid is Afspelen @@ -168,7 +183,7 @@ Automatisch downloaden Configureer het automatisch downloaden van episoden. Wi-Fi filter inschakelen - Automatisch download alleen voor geselecteerde Wi-Fi-netwerken toestaan. + Automatisch downloaden alleen toestaan voor geselecteerde Wi-Fi-netwerken. Episode cache Licht Donker @@ -176,8 +191,16 @@ uren uur Handmatig + Log in + Log met je gpodder.net account in om je abonnementen te synchroniseren. + Log uit + Uitlog was succesvol + Aanmeldingsgegevens wijzigen + Wijzig de aanmeldingsgegevens van je gpodder.net account. Afspeelsnelheden Pas de beschikbare snelheden aan voor de variabele audio afspeelsnelheid + Definieer hostname + Gebruik standaard host Feeds of episodes zoeken Gevonden in de shownotes @@ -189,6 +212,7 @@ Zoeken Gevonden in de titel + Met OPML-bestanden kan je podcasts van de ene naar de andere podcatcher verplaatsen. Om een OPML-bestand te importeren moet je het in de volgende map zetten en op onderstaande knop drukken. Start importeren OPML import @@ -219,8 +243,36 @@ Zoek Miro gids Populair Beste beoordeling + Podcast toevoegen Feed wordt toegevoegd + CATEGORIEËN + TOP PODCASTS + SUGGESTIES + Zoek gpodder.net + Log in + Welkom op de gpodder.net login proces. Eerst, typ je login gegevens: + Log in + Als je nog geen account hebt, kun je er hier een aanmaken:\n https://gpodder.net/register/ + Gebruikersnaam + Wachtwoord + Apparaatselectie + Maak een nieuw apparaat aan om voor je gpodder.net account te gebruiken of kies een bestaande: + Device ID:\u0020 + Titel + Maak een nieuw apparaat aan + Kies een bestaand apparaat: + Apparaat ID mag niet leeg zijn + Apparaat ID al in gebruik + Kies + Login succesvol + Gefeliciteerd! Jou gpodder.net account is nu verbonden met je apparaat. AntennaPod zal voortaan abonnementen op je apparaat automatisch met je gpodder.net account synchroniseren. + Synchronisatie nu starten + Terug naar hoofdscherm + gpodder.net authenticatie fout + Ongeldig gebruikersnaam of wachtwoord + gpodder.net synchronisatie fout + Er is een fout opgetreden tijdens het synchroniseren:\u0020 Geselecteerde map: Map aanmaken @@ -233,5 +285,14 @@ Map is niet leeg De map die je hebt gekozen is niet leeg. Media downloads en andere bestanden zullen rechtstreeks in deze map geplaatst worden. Toch doorgaan? Kies default map + Het afspelen onderbreken in plaats van het volume te verlagen wanneer er een andere app geluiden af wilt spelen + Pauze voor onderbrekingen + Abonneren + Geabonneerd + Aan het downloaden + + Audio + Video + Meer acties diff --git a/res/values-pl-rPL/strings.xml b/res/values-pl-rPL/strings.xml new file mode 100644 index 000000000..34096a769 --- /dev/null +++ b/res/values-pl-rPL/strings.xml @@ -0,0 +1,307 @@ + + + + AntennaPod + Kanały + PODCASTY + ODCINKI + Nowy + Lista oczekujących + Ustawienia + Dodaj podcast + Pobrane + Anuluj pobieranie + Dziennik pobierania + Historia odtwarzania + gpodder.net + gpodder.net login + + Otwórz w przeglądarce + Kopiuj adres + Udostępnij adres + Skopiowano adres do schowka. + + Wyczyść historię + + Potwierdź + Anuluj + Autor + Język + Ustawienia + Błąd + Wystąpił błąd: + Odśwież + Brak zewnętrznej pamięci. Sprawdź czy jest ona podłączona żeby aplikacja mogła pracować poprawnie. + Rozdziały + Pokaż notatki + Opis + Najnowszy odcinek:\u0020 + :\u0020odcinków + Opublikowane:\u0020 + Długość:\u0020 + Rozmiar:\u0020 + Przetwarzanie + Ładowanie... + Obraz z:\u0020 + Zapisz nazwę użytkownika i hasło + Zamknij + Spróbuj ponownie + Dołącz do automatycznego pobierania + + Adres kanału + Dodaj podcast przez adres + Katalogi podcastów + + Oznacz wszystkie jako przeczytane + Pokaż informacje + Usuń kanał + Udostępnij stronę + Udostępnij kanał + Potwierdź chęć usunięcia tego kanału wraz ze WSZYSTKIMI odcinkami, które zostały pobrane. + Usuwanie kanału + + Pobierz + Odtwórz + Pauza + Strumień + Usuń + Oznacz jako przeczytane + Oznacz jako nieprzeczytane + Dodaj do kolejki + Usuń z kolejki + Odwiedź stronę + Wspomóż na Flattr + Dodaj wszystko do kolejki + Pobierz wszystkie + Pomiń odcinek + + Pobieranie ukończone + Pobieranie nieudane + Pobieranie w toku + Pobieram + Nie znaleziono urządzenia docelowego + Nie wystarczająca ilość pamięci + Błąd pliku + Błąd danych HTTP + Nieznany błąd + Wyjątek parsera + Nieobsługiwany typ kanału + Błąd połączenia + Nieznany host + Anuluj wszystkie pobierania + Pobieranie anulowane + Pobieranie ukończone + Niepoprawny adres + Błąd wejścia/wyjścia + Błąd żądania + Błąd dostępu do bazy danych + :\u0020pobrań pozostało + Pobieranie danych podcastu + %1$d pobierania poprawne, %2$d nieudane + Nieznany tytuł + Kanał + Plik multimedialny + Obraz + Wystąpił błąd przy próbie pobierania:\u0020 + + Błąd! + Żadne media nie odtwarzane + Przygotowuję + Gotowe + Szukam + Serwer zdechł + Nieznany błąd + Żadne media nie odtwarzane + 00:00:00 + Buferowanie + Odtwarzenie podcastu + Więcej informacji + + Pokaż log + Pokaż odtwarzacz + + Wyczyść kolejkę + Organizuj kolejkę + Cofnij + Element usunięty + Przesuń na górę + Przesuń na dół + + Logowanie do Flattr + Naciśnij przycisk poniżej by zacząć proces autoryzacji. Zostaniesz przekierowany na stronę logowanie do flattr w przeglądarce i zostaniesz poproszony o przyznanie zezwolenia AntennaPod-owi na flattr-owanie. Po daniu zezwolenia powrócisz do tej strony automatycznie. + Autoryzacja + Wróć do ekranu głównego + Autoryzacja się powiodła. Możesz teraz używać flattr w aplikacji. + Nie znaleziono tokenu Flattr + Twoje konto Flattr wydaje się nie być podłączone do AntennaPod. Możesz połączyć konto do AntennaPod by przez program flattr-ować lub możesz odwiedzić stronę wątku by zrobić to tam. + Autoryzuj + Akcja zabroniona + AntennaPod nie ma zezwolenia na tą akcję. Powodem może być fakt iż dostęp dla AntennaPod do Twojego konta został cofnięty. Możesz ponownie autoryzować aplikację lub odwiedzić stronę. + Anulowano dostęp + Odwołałeś dostęp AntennaPod do swojego konta. W celu zakończenia procesu musisz usunąć aplikację z listy aplikacji dozwolonych na koncie Flattr. + + + Pobierz wtyczkę + Wtyczka nie zainstalowana + Do odtwarzania ze zmienną prędkością jest potrzebna biblioteka innej firmy. \n\nDotknij przycisku \"Pobierz wtyczkę\", aby pobrać darmową wtyczkę ze sklepu\n\nWszelkie znalezione za pomocą tej wtyczki problemy nie są odpowiedzialnością AntennaPod i należy zgłosić się do właściciela plugin. + Prędkość odtwarzania + + Brak elementów na tej liście. + Nie subskrybowałeś jeszcze żadnego kanału. + + Inne + O... + Kolejka + Usługi + Flattr + Wstrzymaj odtwarzanie kiedy słuchawki zostaną odłączone + Przeskocz do następnego elementu kolejki po zakończeniu odtwarzania + Odtwarzanie + Sieć + Częstość aktualizacji + Określ częstotliwość automatycznego odświeżania lub je wyłącz + Pobieraj pliki tylko przez WiFi + Odtwarzanie ciągłe + WiFi media pobrane + Słuchawki odłączone + Aktualizacje mobilne + Zezwól na aktualizacje poprzez sieć komórkową + Odświeżanie + Ustawienia Flattr + Logowanie do Flattr + Zaloguj się do konta Flattr aby wspierać twórców bezpośrednio z aplikacji. + Wesprzyj aplikację na Flattr + Wesprzyj twórcę AntennaPod przez Flattr. Dzięki! + Anuluj dostęp + Anuluj dostęp tej aplikacji do konta Flattr + Automatyczne wsparcie na Flattr + Wyświetlaj tylko odcinki + Wyświetlaj tylko pozycje, które mają również odcinki. + Interfejs użytkownika + Wybierz motyw + Zmień wygląd AntennaPod. + Automatyczne pobieranie + Skonfiguruj automatyczne pobieranie odcinków. + Włącz filtr Wi-Fi + Zezwól na automatyczne pobieranie tylko dla określonych sieci Wi-Fi. + Pamięć podręczna odcinków + Jasny + Ciemny + Nielimitowane + godziny + godzina + Instrukcja + Zaloguj + Zaloguj się swoim kontem na gpodder.net w celu synchronizacji Twoich subskrypcji. + Wyloguj + Wylogowanie się powiodło + Zmień informacje logowania + Zmień dane logowania konta gpodder.net. + Prędkość odtwarzania + Dostosuj prędkości dostępne dla odtwarzania audio o zmiennej prędkości + Ustaw nazwę hosta + Użyj domyślnego hosta + + Szukaj kanałów lub odcinków + Znaleziono w notatkach + Znaleziono w rozdziałach + Wyszukiwanie... + Brak wyników + Wyniki wyszukiwania + Szukałeś:\u0020 + Szukaj + Znaleziono w tytułach + + Pliki OPML pozwalają na przenoszenie podcastów między aplikacjami. + W celu importu pliku OPML musisz umieścić go w poniższym folderze i nacisnąć przycisk poniżej w celu rozpoczęcia importu. + Rozpocznij import + Import OPML + BŁĄD! + Odczytuję plik OPML + Wystąpił błąd w czasie odczytu dokumentu OPML: + Katalog importowania jest pusty. + Zaznacz wszystko + Odznacz wszystko + Wybierz plik do importu + Eksport OPML + Eksportowanie... + Błąd eksportu + Eksport OPML udany. + Plik .opml został zapisany do:\u0020 + + Ustaw czas do wyłączenia + Wyłącz wyłącznik czasowy + Podaj czas + Wyłącznik czasowy + Pozostały czas:\u0020 + Błąd wpisu, czas musi być liczbą całkowitą + + Ładuję kategorie... + Przeglądaj Miro Guide + Lub przeglądaj Miro Guide: + Miro Guide + Szukaj w Miro Guide + Popularne + Najwyższe oceny + Dodaj podcast + Kanał jest dodawany + + KATEGORIE + TOP PODCASTY + SUGESTIE + Szukaj na gpodder.net + Login + Witamy w procesie logowania do gpodder.net. Najpierw podaj swoje dane logowania: + Login + Jeśli nie masz jeszcze konta, możesz utworzyć je tutaj:\nhttps://gpodder.net/register/ + Nazwa użytkownika + Hasło + Wybór urządzenia + Utwórz nowe urządzenie dla swojego konta na gpodder.net lub wybierz istniejące: + Identyfikator urządzenia:\u0020 + Tytuł + Utwórz nowe urządzenie + Wybierz istniejące urządzenie: + Identyfikator urządzenia nie może być pusty + Identyfikator urządzenia w użyciu + Wybierz + Logowanie zakończone sukcesem! + Gratulacje! Twoje konto na gpodder.net jest połączone z urządzeniem. AntennaPod będzie automatycznie synchronizować subskrypcje na urządzeniu z kontem na gpodder.net. + Rozpocznij synchronizację + Idź do strony głównej + Błąd autoryzacji na gpodder.net + Niepoprawna nazwa użytkownika lub hasło + Błąd synchronizacji z gpodder.net + Wystąpił błąd podczas synchronizacji:\u0020 + + Wybrany folder: + Utwórz folder + Wybierz folder danych + Utworzyć nowy folder o nazwie \"%1$s\"? + Utworzono nowy folder + Nie można zapisać do tego folderu + Folder już istnieje + Nie można utworzyć folderu + Folder nie jest pusty + Wybrany folder nie jest pusty. Pobierane media i inne pliki będą umieszczane bezpośrednio w folderze, czy kontynuować? + Wybierz domyślny folder + Wstrzymaj odtwarzanie zamiast wyciszenia jeśli inna aplikacja chce odtworzyć dźwięk. + Wstrzymaj przy przerwaniu + + Subskrybuj + Subskrybowane + Pobieranie... + + Pokaż rozdziały + Cofnij + Przewiń + Audio + Wideo + Odcinek jest odtwarzany + Odcinek jest pobierany + Odcinek pobrany + Nowa pozycja + Odcinek jest w kolejce + Liczba nowych odcinków + Liczba odcinków, których zacząłeś słuchać + diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 64cfaad00..3c00c27ea 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -96,14 +96,14 @@ Erro de IO Erro de requisição Erro no acesso ao Banco de dados - \u0020Downloads faltando + \u0020Downloads restantes Baixando dados do podcast %1$d downloads com sucesso, %2$d falharam Título desconhecido Feed Arquivo de mídia Imagem - Ocorreu um erro na tentativa de baixar o arquivo:\u0020 + Ocorreu um erro durante download do arquivo:\u0020 Erro! Nenhuma mídia tocando @@ -140,12 +140,11 @@ AntennaPod não tem permissão para esta ação. A permissão de acesso do AntennaPod pode ter sido revogada. Você pode re-autenticar ou visitar o website do feed. Acesso revogado Você revogou o token de acesso do AntennaPod com sucesso. Para finalizar o processo, você deve remover esta app da lista de aplicativos aprovados nas configurações de sua conta no website do Flattr. - Registrado no Flattr com sucesso! - Registrando no Flattr + Download Plugin Plugin Não Instalado - Para ajustar a velocidade de reprodução, uma biblioteca de terceiros deve ser instalada.\n\nToque em \'Download Plugin\' para baixar um plugin grátis na Play Store.\n\nQuaisquer problemas encontrados usando esse plugin não é responsabilidade do AntennaPod e deve ser reportado ao proprietário do plugin. + Para velocidade variável de reprodução funcionar uma biblioteca de terceiros deve ser instalada.\n\nToque em \'Download Plugin\' para baixar um plugin grátis na Play Store.\n\nQuaisquer problemas encontrados usando esse plugin não é responsabilidade do AntennaPod e deve ser reportado ao proprietário do plugin. Velocidades de Reprodução Não existem itens nesta lista. @@ -161,7 +160,7 @@ Reprodução Rede Intervalo de atualização - Especifica o intervalo em que os feeds serão atualizados automaticamente ou desabilita esta funcionalidade + Especifica o intervalo com que os feeds serão atualizados automaticamente ou desabilita esta funcionalidade Fazer download dos arquivos apenas via rede WiFi Reprodução contínua Download de mídia via WiFi @@ -193,13 +192,13 @@ hora Manual Login - Faça o login na sua conta gpodder.net para sincronizar suas subscrições. + Faça o login na sua conta gpodder.net para sincronizar suas assinaturas. Sair Saiu com sucesso Alterar informações de login Alterar informações de login da sua conta gpodder.net Velocidades de Reprodução - Personalize as velocidades disponíveis para reprodução de áudio. + Personalize as velocidades variáveis de reprodução de áudio. Configurar hostname Usar host padrão @@ -213,7 +212,7 @@ Pesquisar Encontrado no título - Arquivos OPML permitem que você mova seus podcasts de um gestor de podcasts para outro. + Arquivos OPML permitem que você mova seus podcasts de um programa de podcasts para outro. Para importar um arquivo OPML, você precisa armazená-lo neste diretório e pressionar o botão abaixo para iniciar o processo de importação. Iniciar importação Importação de OPML @@ -231,9 +230,9 @@ O arquivo .opml foi gravado em:\u0020 Configura desligamento automático - Desabilita temporizador + Desabilita desligamento automático Informe a duração - Temporizador + Desligamento automático Tempo restante:\u0020 Entrada inválida, a duração precisa ser um número inteiro @@ -250,6 +249,7 @@ CATEGORIAS TOP PODCASTS SUGESTÕES + Buscar no gpodder.net Login Bem-vindo ao processo de login gpodder.net. Primeiramente, digite suas informações: Login @@ -266,12 +266,12 @@ ID do dispositivo já está em uso Escolher Login realizado com sucesso! - Parabéns! Sua conta gpodder.net agora está ligada ao seu dispositivo. AntennaPod irá, daqui em diante, sincronizar automaticamente subscrições do seu dispositivo com sua conta gpodder.net. + Parabéns! Sua conta gpodder.net agora está conectada ao seu dispositivo. O AntennaPod irá, daqui em diante, sincronizar automaticamente assinaturas do seu dispositivo com sua conta gpodder.net. Iniciar sincronização agora Ir para tela principal - gpodder.net: Erro de autenticação + gpodder.net: erro de autenticação Nome do usuário ou senha incorreta - gpodder.net: Erro de sincronização + gpodder.net: erro de sincronização Ocorreu um erro durante a sincronização:\u0020 Selecionar pasta: @@ -285,10 +285,18 @@ A pasta não está vazia A pasta que você selecionou não está vazia. Os downloads de mídia e outros arquivos serão colocados diretamente nesta pasta. Deseja mesmo continuar? Escolher pasta padrão - Pause a reprodução em vez de baixar o volume quando outro aplicativo reproduzir sons + Pause a reprodução em vez de abaixar o volume quando outro aplicativo reproduzir sons Pausar em interrupções - Subscrever - Subscrito + Assinar + Assinado Baixando... + + Mostrar imagem + Mais ações + Episódio está sendo reproduzido + Episódio foi baixado + Item é novo + Episódio está na fila + Numero de novos episódios diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 392b34dd1..bb9009d18 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -140,12 +140,22 @@ O AntennaPod não possui as permissões para esta ação. É possível que o token de acesso ao flattr via AntennaPod tenha sido revogado. Pode efetuar nova autenticação ou aceder ao sítio web do item. Acesso revogado Você revogou o token de acesso do AntennaPod à sua conta. Para concluir o processo, tem que remover esta aplicação da lista de aplicações presentes nas definições de conta no sítio web do flattr. - Flattered com sucesso! - Flattring + + Flattr de um item! + Flattr de %d itens! + Flattr: %s + Falha ao efetuar flattr de %d itens! + Não flattr: %s. + O flattr deste item será feito mais tarde + Flattring %s + O AntennaPod está a flattring + O AntennaPod fez o flattr + O AntennaPod não fez o flattr + A obter itens com flattr Transferir extra Extra não instalado - Para melhorar a reprodução, deve transferir e instalar um biblioteca de terceiros.\nClique Transferir extra para transferir o extra através da loja Google..\n\nSe encontrar problemas ao utilizar esta biblioteca, os programadores do AntennaPod não podem ser responsabilizados e deve contactar o programador do extra. + Para melhorar a reprodução, deve transferir e instalar um biblioteca de terceiros.\nClique Transferir extra para transferir o extra através da loja Google.\n\nSe encontrar problemas ao utilizar esta biblioteca, os programadores do AntennaPod não podem ser responsabilizados e deve contactar o programador do extra. Velocidades de reprodução Não existem itens na lista. @@ -176,10 +186,12 @@ Ajude no desenvolvimento do AntennaPod através do Flattr. Obrigado! Revogar acesso Revogar permissões de acesso da aplicação à sua conta flattr. + Flattr automático + Flattr de episódios com 80 porcento de reprodução. Mostrar apenas episódios Apenas mostrar itens que possuam episódios. Interface - Escolha o tema + Tema Mudar o aspeto do AntennaPod. Transferência automática Configure a transferência automática dos episódios. @@ -292,4 +304,21 @@ Subscrever Subscrito Transferência... + + Mostrar capítulos + Mostrar notas + Mostrar imagem + Recuar + Avanço rápido + Áudio + Vídeo + Navegar para cima + Mais ações + Episódio em reprodução + Episódio a ser transferido + Episódio transferido + Novo item + Episódio está na fila + Número de novos episódios + Número de episódios que já foi iniciada a reprodução diff --git a/res/values-ro-rRO/strings.xml b/res/values-ro-rRO/strings.xml index b2a3d6526..d610a27f0 100644 --- a/res/values-ro-rRO/strings.xml +++ b/res/values-ro-rRO/strings.xml @@ -27,7 +27,6 @@ Autor Limbă Setări - Copertă Eroare A avut loc o eroare: Reîncarcă @@ -132,8 +131,7 @@ AntennaPod nu are permisiuni pentru această acțiune. Motivul poate fi că tokenul de acces al AntennaPod pentru contul vostru a fost revocat. Vă puteți fie re-autentifica fie vizita direct site-ul. Acces revocat Ați revocat cu succes accesul AntennaPod la contul vostru. Pentru a completa acest proces trebuie să ștergeți aplicația din lista de aplicații aprobate din setările contului de pe site-ul flattr. - Ați flattr cu succes! - Flattring + Descarcă plugin Plugin neinstalat @@ -263,4 +261,5 @@ Abonează-te Abonat Se descarcă... + diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 0d40685d7..8b69c340c 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -28,16 +28,16 @@ Автор Язык Настройки - Обложка + Изображение Ошибка Произошла ошибка: Обновить Внешний носитель недоступен. Убедитесь что внешний носитель установлен, иначе приложение не сможет нормально работать. Разделы - Описание + Заметки к эпизоду Описание Следующий эпизод:\u0020 - \u0020 выпуск(ов) + \u0020выпуск(ов) Опубликовано:\u0020 Продолжительность:\u0020 Размер:\u0020 @@ -56,8 +56,8 @@ Отметить все как прочитанное Показать информацию Удалить канал - Поделиться ссылкой на сайт - Поделиться ссылкой на канал + Ссылка на сайт + Ссылка на канал Подтвердите удаление канала и ВСЕХ загруженных с этого канала выпусков. Удаление канала @@ -81,7 +81,7 @@ Загрузка в ожидании Загрузка в процессе Устройство хранения не найдено - Недостаточно памяти + Недостаточно места Ошибка файла Ошибка протокола HTTP Неизвестная ошибка @@ -140,8 +140,8 @@ AntennaPod не имеет прав для выполнения этого действия. Возможно, доступ к вашему аккаунту был отозван. Вы можете авторизоваться заново или посетить сайт, которому вы пожертвовали через Flattr. Доступ отозван Вы успешно отключили AntennaPod от вашего аккаунта в Flattr. Чтобы завершить этот процесс вам нужно удалить AntennaPod из списка приложений подключенных к вашему аккаунту на сайте Flattr. - Поддержка через Flattr прошла успешно! - Отправляется запрос на Flattr + + Поддержано через Flattr: %s. Загрузить плагин Плагин не установлен @@ -176,6 +176,8 @@ Поддержите разработку AntennaPod через Flattr. Спасибо! Отозвать доступ Отменить доступ этого приложения к вашему аккаунту Flattr. + Автоматически поддерживать через Flattr + Поддерживать через Flattr эпизоды, прослушанные на 80% Показывать только выпуски Показывать только те элементы списка, которые содержат выпуски Интерфейс @@ -189,8 +191,8 @@ Светлая Тёмная Неограничен - Часы - Час + ч. + час Вручную Вход в gpodder.net Вход в ваш аккаунт gpodder.net для синхронизации ваших подписок. @@ -258,7 +260,7 @@ Имя пользователя Пароль Выбор устройства - Создайте новое устройство, чтобы использовать ваш аккаунта на gpodder.net или выберите существующее: + Создайте новое устройство, чтобы использовать ваш аккаунт на gpodder.net или выберите существующее: Device ID:\u0020 Название устройства Создайте новое устройство @@ -268,12 +270,12 @@ Выберите Авторизация успешна! Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.net - Начать синхронизацию сейчас + Начать синхронизацию Перейти на главный экран Ошибка авторизации на gpodder.net Неправильное имя пользователя или пароль Ошибка синхронизации с gpodder.net - Произошла ошибка во время синзронизации:\u0020 + Произошла ошибка во время синхронизации:\u0020 Выбранная папка: Создать папку @@ -292,4 +294,21 @@ Подписаться Подписка оформлена Загрузка... + + Показать разделы + Показать заметки к эпизодам + Показать изображение + Назад + Вперед + Аудио + Видео + Перейти выше + Другие действия + Эпизод воспроизводится + Эпизод загружается + Эпизод загружен + Новый + Эпизод в очереди + Количество новых эпизодов + Количество начатых эпизодов diff --git a/res/values-sv-rSE/strings.xml b/res/values-sv-rSE/strings.xml index ccfcddac4..426dd7b37 100644 --- a/res/values-sv-rSE/strings.xml +++ b/res/values-sv-rSE/strings.xml @@ -28,7 +28,7 @@ Skapare Språk Inställningar - Omslag + Bild Fel Ett fel inträffade: Uppdatera @@ -118,7 +118,7 @@ Spelar podcast Tryck här för mer information - Visa log + Visa logg Visa spelare Rensa kön @@ -140,8 +140,18 @@ AntennaPod saknar behörighet för den här åtgärden. Anledningen till detta kan vara att AntennaPods tillgång till ditt konto har återkallats. Du kan antingen åter autentisera AntennaPod eller besöka hemsidan istället. Tillgång återkallad Du har nu återkallat AntennaPods tillgång till ditt konto. För att slutföra processen, måste du ta bort den här appen från listan godkända appar i dina kontoinställningar på Flattrs hemsida. - Framgångsrikt flattrat denna sak! - Flattrar + + Flattrade en sak! + Flattrade %d saker! + Flattrade: %s. + Misslyckades att flattra %d saker! + Ej flattrade: %s. + Saker som kommer att flattras senare + Flattrar %s + AnntennaPod flattrar + AntennaPod har flattrat + AntennaPod misslyckades att flattra + Hämtar flattrade saker Ladda ner tillägg Tillägg ej installerat @@ -176,6 +186,8 @@ Stöd utvecklingen av AntennaPod genom att flattra den. Tack! Återkalla åtkomst Återkalla behörigheten till ditt Flattr-konto för denna app. + Automatisk Flattring + Flattra episoder som har spelats minst 80%. Visa endast episoder Visa endast objekt som har minst ett avsnitt. Användargränssnitt @@ -292,4 +304,21 @@ Prenumerera Prenumererar Laddar ner... + + Visa kapitel + Visa shownotes + Visa bild + Backa + Snabbspola + Ljud + Video + Navigera upp + Fler åtgärder + Episoden spelas + Episoden laddas ner + Episoden är nedladdad + Föremålet är nytt + Episoden är i kön + Antal nya episoder + Antal episoder du har börjat lyssna på diff --git a/res/values-uk-rUA/strings.xml b/res/values-uk-rUA/strings.xml index 32d6e5101..534c46429 100644 --- a/res/values-uk-rUA/strings.xml +++ b/res/values-uk-rUA/strings.xml @@ -28,7 +28,7 @@ Автор Мова Налаштування - Обкладинка + Зображення Помилка Трапилась помілка: Оновити @@ -140,8 +140,18 @@ AntennaPod не маэ дозвілу це зробити. Можливо відкликаний доступ до AntennaPod. Або ввідіть логін пароль в налаштуваннях або зробить це на сайті Доступ відкликано Ви відкликали доступ AntennaPod до облікового запису. Для закінчення процессу вам потрібно видалити додаток з затвержденного списку в вашому облікову запису на сайті flattr - Успішно flattr це - Йде flattr + + Flattr\'ed one thing! + Flattr\'ed %d things! + Flattr\'ed: %s. + Failed to flattr %d things! + Not flattr\'ed: %s. + Thing will be flattr\'ed later + Flattring %s + AntennaPod is flattring + AntennaPod has flattr\'ed + AntennaPod flattr failed + Retrieving flattr\'ed things Завантажити Plugin Plugin не встановлено @@ -176,6 +186,8 @@ Підтримайте розробку AntennaPod за допомогою flattr. Дякую! Відкликати доступ Відкликати дозвіл на доступ до вашого flattr з цього додатку + Automatic Flattr + Flattr episodes of which 80% have been played. Показувати тільки епізоди Відображати тільки канали з наявними епізодами Зовнішній вид @@ -248,7 +260,7 @@ Канал додано КАТЕГОРІЇ - ТОП ПОКАДСТІВ + ТОП ПОДКАСТІВ РЕКОМЕНДАЦІЇ Пошук на gpodder.net Логін @@ -292,4 +304,21 @@ Підписатися Підписано Завантаження... + + Показати глави + Показати нотатки + Показати зображення + Перемотка назад + Перемотка вперед + Звук + Відео + Догори + Додаткові дії + Епізод програється + Епізод завантажується + Епізод завантажено + Нове + Епізод чекає в черзі + Кількість нових епізодів + Кількість епізодів що ви почали слухати diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 6cc612374..4bb3400c4 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -28,7 +28,7 @@ 作者 语言 设置 - 封面 + 图片 错误 出错: 刷新 @@ -140,8 +140,7 @@ AntennaPod 没有权限执行本动作. 原因可能是: AntennaPod 对您账户的访问令牌被撤销. 你可以重新\"验证\"或访问该网站来授权. 撤销访问 您已经成功撤销 AntennaPod 对账户令牌的访问. 为了完成这个过程, 您必须到 Flattr 网站 \"账户设置->已批准应用\" 列表内删除本应用. - Flattr 成功! - Flattring + 插件下载 插件没有安装 @@ -153,7 +152,7 @@ 其他 关于 - 清空播放 + 播放列表 服务 Flattr 耳机断开时暂停播放 @@ -292,4 +291,21 @@ 订阅 已订阅 下载中... + + 显示章节 + 显示笔记 + 显示图片 + 回放 + 快进 + 音频 + 视频 + 向上导航 + 更多动作 + 曲目正在播放 + 曲目正在下载 + 曲目已下载 + 新项目 + 曲目已经在播放列表中 + 新曲目数 + 已收听曲目数 diff --git a/res/values/strings.xml b/res/values/strings.xml index 8c05e42b5..43bedadee 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -35,7 +35,7 @@ Author Language Settings - Cover + Picture Error An error occurred: Refresh @@ -154,9 +154,20 @@ AntennaPod has no permission for this action. The reason for this could be that the access token of AntennaPod to your account has been revoked. You can either re-reauthenticate or visit the website of the thing instead. Access revoked You have successfully revoked AntennaPod\'s access token to your account. In order to complete the process, you have to remove this app from the list of approved applications in your account settings on the flattr website. - Successfully flattred this thing! - Flattring + + Flattr\'ed one thing! + Flattr\'ed %d things! + Flattr\'ed: %s. + Failed to flattr %d things! + Not flattr\'ed: %s. + Thing will be flattr\'ed later + Flattring %s + AntennaPod is flattring + AntennaPod has flattr\'ed + AntennaPod flattr failed + Retrieving flattr\'ed things + Download Plugin Plugin Not Installed @@ -193,6 +204,8 @@ Support the development of AntennaPod by flattring it. Thanks! Revoke access Revoke the access permission to your flattr account for this app. + Automatic Flattr + Flattr episodes of which 80% have been played. Display only episodes Display only items which also have an episode. User Interface @@ -319,4 +332,22 @@ Subscribe Subscribed Downloading... + + + Show chapters + Show shownotes + Show picture + Rewind + Fast forward + Audio + Video + Navigate upwards + More actions + Episode is being played + Episode is being downloaded + Episode is downloaded + Item is new + Episode is in the queue + Number of new episodes + Number of episodes you have started listening to diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index cba297570..8e0b886de 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -83,6 +83,12 @@ + - = 0 && controller != null - && controller.getMedia() != null) { - editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, - currentlyShownPosition); - editor.putString(PREF_PLAYABLE_ID, controller.getMedia() - .getIdentifier().toString()); - } else { - editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1); - editor.putString(PREF_PLAYABLE_ID, ""); - } - editor.commit(); + private void savePreferences() { + if (AppConfig.DEBUG) + Log.d(TAG, "Saving preferences"); + SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + if (currentlyShownPosition >= 0 && controller != null + && controller.getMedia() != null) { + editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, + currentlyShownPosition); + editor.putString(PREF_PLAYABLE_ID, controller.getMedia() + .getIdentifier().toString()); + } else { + editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1); + editor.putString(PREF_PLAYABLE_ID, ""); + } + editor.commit(); - savedPosition = currentlyShownPosition; - } + savedPosition = currentlyShownPosition; + } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } - @Override - protected void onSaveInstanceState(Bundle outState) { - // super.onSaveInstanceState(outState); would cause crash - if (AppConfig.DEBUG) - Log.d(TAG, "onSaveInstanceState"); - } + @Override + protected void onSaveInstanceState(Bundle outState) { + // super.onSaveInstanceState(outState); would cause crash + if (AppConfig.DEBUG) + Log.d(TAG, "onSaveInstanceState"); + } - @Override - protected void onPause() { - savePreferences(); - resetFragmentView(); - super.onPause(); - } + @Override + protected void onPause() { + savePreferences(); + resetFragmentView(); + super.onPause(); + } - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - restoreFromPreferences(); - } + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + restoreFromPreferences(); + } - /** - * Tries to restore the selected fragment position from the Activity's - * preferences. - * - * @return true if restoreFromPrefernces changed the activity's state - * */ - private boolean restoreFromPreferences() { - if (AppConfig.DEBUG) - Log.d(TAG, "Restoring instance state"); - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, - -1); - String playableId = prefs.getString(PREF_PLAYABLE_ID, ""); + /** + * Tries to restore the selected fragment position from the Activity's + * preferences. + * + * @return true if restoreFromPrefernces changed the activity's state + */ + private boolean restoreFromPreferences() { + if (AppConfig.DEBUG) + Log.d(TAG, "Restoring instance state"); + SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); + int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, + -1); + String playableId = prefs.getString(PREF_PLAYABLE_ID, ""); - if (savedPosition != -1 - && controller != null - && controller.getMedia() != null - && controller.getMedia().getIdentifier().toString() - .equals(playableId)) { - switchToFragment(savedPosition); - return true; - } else if (controller == null || controller.getMedia() == null) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Couldn't restore from preferences: controller or media was null"); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: " - + savedPosition + ", id: " + playableId); + if (savedPosition != -1 + && controller != null + && controller.getMedia() != null + && controller.getMedia().getIdentifier().toString() + .equals(playableId)) { + switchToFragment(savedPosition); + return true; + } else if (controller == null || controller.getMedia() == null) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Couldn't restore from preferences: controller or media was null"); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, + "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: " + + savedPosition + ", id: " + playableId); - } - return false; - } + } + return false; + } - @Override - protected void onResume() { - super.onResume(); - if (getIntent().getAction() != null - && getIntent().getAction().equals(Intent.ACTION_VIEW)) { - Intent intent = getIntent(); - if (AppConfig.DEBUG) - Log.d(TAG, "Received VIEW intent: " - + intent.getData().getPath()); - ExternalMedia media = new ExternalMedia(intent.getData().getPath(), - MediaType.AUDIO); - Intent launchIntent = new Intent(this, PlaybackService.class); - launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, - true); - launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); - launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, - true); - startService(launchIntent); - } - if (savedPosition != -1) { - switchToFragment(savedPosition); - } + @Override + protected void onResume() { + super.onResume(); + if (getIntent().getAction() != null + && getIntent().getAction().equals(Intent.ACTION_VIEW)) { + Intent intent = getIntent(); + if (AppConfig.DEBUG) + Log.d(TAG, "Received VIEW intent: " + + intent.getData().getPath()); + ExternalMedia media = new ExternalMedia(intent.getData().getPath(), + MediaType.AUDIO); + Intent launchIntent = new Intent(this, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + true); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + startService(launchIntent); + } + if (savedPosition != -1) { + switchToFragment(savedPosition); + } - } + } - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } - @Override - protected void onAwaitingVideoSurface() { - startActivity(new Intent(this, VideoplayerActivity.class)); - } + @Override + protected void onAwaitingVideoSurface() { + if (AppConfig.DEBUG) Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player"); + startActivity(new Intent(this, VideoplayerActivity.class)); + } - @Override - protected void postStatusMsg(int resId) { - setSupportProgressBarIndeterminateVisibility(resId == R.string.player_preparing_msg - || resId == R.string.player_seeking_msg - || resId == R.string.player_buffering_msg); - } + @Override + protected void postStatusMsg(int resId) { + setSupportProgressBarIndeterminateVisibility(resId == R.string.player_preparing_msg + || resId == R.string.player_seeking_msg + || resId == R.string.player_buffering_msg); + } - @Override - protected void clearStatusMsg() { - setSupportProgressBarIndeterminateVisibility(false); - } + @Override + protected void clearStatusMsg() { + setSupportProgressBarIndeterminateVisibility(false); + } - /** - * Changes the currently displayed fragment. - * - * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS - * */ - private void switchToFragment(int pos) { - if (AppConfig.DEBUG) - Log.d(TAG, "Switching contentView to position " + pos); - if (currentlyShownPosition != pos && controller != null) { - Playable media = controller.getMedia(); - if (media != null) { - FragmentTransaction ft = getSupportFragmentManager() - .beginTransaction(); - if (currentlyShownFragment != null) { - detachedFragments[currentlyShownPosition] = currentlyShownFragment; - ft.detach(currentlyShownFragment); - } - switch (pos) { - case POS_COVER: - if (coverFragment == null) { - Log.i(TAG, "Using new coverfragment"); - coverFragment = CoverFragment.newInstance(media); - } - currentlyShownFragment = coverFragment; - break; - case POS_DESCR: - if (descriptionFragment == null) { - descriptionFragment = ItemDescriptionFragment - .newInstance(media, true); - } - currentlyShownFragment = descriptionFragment; - break; - case POS_CHAPTERS: - if (chapterFragment == null) { - chapterFragment = new ListFragment() { + /** + * Changes the currently displayed fragment. + * + * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS + */ + private void switchToFragment(int pos) { + if (AppConfig.DEBUG) + Log.d(TAG, "Switching contentView to position " + pos); + if (currentlyShownPosition != pos && controller != null) { + Playable media = controller.getMedia(); + if (media != null) { + FragmentTransaction ft = getSupportFragmentManager() + .beginTransaction(); + if (currentlyShownFragment != null) { + detachedFragments[currentlyShownPosition] = currentlyShownFragment; + ft.detach(currentlyShownFragment); + } + switch (pos) { + case POS_COVER: + if (coverFragment == null) { + Log.i(TAG, "Using new coverfragment"); + coverFragment = CoverFragment.newInstance(media); + } + currentlyShownFragment = coverFragment; + break; + case POS_DESCR: + if (descriptionFragment == null) { + descriptionFragment = ItemDescriptionFragment + .newInstance(media, true); + } + currentlyShownFragment = descriptionFragment; + break; + case POS_CHAPTERS: + if (chapterFragment == null) { + chapterFragment = new ListFragment() { - @Override - public void onListItemClick(ListView l, View v, - int position, long id) { - super.onListItemClick(l, v, position, id); - Chapter chapter = (Chapter) this - .getListAdapter().getItem(position); - controller.seekToChapter(chapter); - } + @Override + public void onListItemClick(ListView l, View v, + int position, long id) { + super.onListItemClick(l, v, position, id); + Chapter chapter = (Chapter) this + .getListAdapter().getItem(position); + controller.seekToChapter(chapter); + } - }; - chapterFragment.setListAdapter(new ChapterListAdapter( - AudioplayerActivity.this, 0, media - .getChapters(), media)); - } - currentlyShownFragment = chapterFragment; - break; - } - if (currentlyShownFragment != null) { - currentlyShownPosition = pos; - if (detachedFragments[pos] != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Reattaching fragment at position " - + pos); - ft.attach(detachedFragments[pos]); - } else { - ft.add(R.id.contentView, currentlyShownFragment); - } - ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); - ft.disallowAddToBackStack(); - ft.commit(); - updateNavButtonDrawable(); - } - } - } - } + }; + chapterFragment.setListAdapter(new ChapterListAdapter( + AudioplayerActivity.this, 0, media + .getChapters(), media)); + } + currentlyShownFragment = chapterFragment; + break; + } + if (currentlyShownFragment != null) { + currentlyShownPosition = pos; + if (detachedFragments[pos] != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Reattaching fragment at position " + + pos); + ft.attach(detachedFragments[pos]); + } else { + ft.add(R.id.contentView, currentlyShownFragment); + } + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + ft.disallowAddToBackStack(); + ft.commit(); + updateNavButtonDrawable(); + } + } + } + } - private void updateNavButtonDrawable() { - TypedArray drawables = obtainStyledAttributes(new int[] { - R.attr.navigation_shownotes, R.attr.navigation_chapters }); - final Playable media = controller.getMedia(); - if (butNavLeft != null && butNavRight != null && media != null) { - switch (currentlyShownPosition) { - case POS_COVER: - butNavLeft.setScaleType(ScaleType.CENTER); - butNavLeft.setImageDrawable(drawables.getDrawable(0)); - butNavRight.setImageDrawable(drawables.getDrawable(1)); - break; - case POS_DESCR: - butNavLeft.setScaleType(ScaleType.CENTER_CROP); - butNavLeft.post(new Runnable() { + private void updateNavButtonDrawable() { - @Override - public void run() { - ImageLoader.getInstance().loadThumbnailBitmap(media, - butNavLeft); - } - }); - butNavRight.setImageDrawable(drawables.getDrawable(1)); - break; - case POS_CHAPTERS: - butNavLeft.setScaleType(ScaleType.CENTER_CROP); - butNavLeft.post(new Runnable() { + final int[] buttonTexts = new int[] {R.string.show_shownotes_label, + R.string.show_chapters_label, R.string.show_cover_label}; - @Override - public void run() { - ImageLoader.getInstance().loadThumbnailBitmap(media, - butNavLeft); - } - }); - butNavRight.setImageDrawable(drawables.getDrawable(0)); - break; - } - } - } + final TypedArray drawables = obtainStyledAttributes(new int[]{ + R.attr.navigation_shownotes, R.attr.navigation_chapters}); + final Playable media = controller.getMedia(); + if (butNavLeft != null && butNavRight != null && media != null) { + switch (currentlyShownPosition) { + case POS_COVER: + butNavLeft.setScaleType(ScaleType.CENTER); + butNavLeft.setImageDrawable(drawables.getDrawable(0)); + butNavLeft.setContentDescription(getString(buttonTexts[0])); - @Override - protected void setupGUI() { - super.setupGUI(); - resetFragmentView(); - txtvTitle = (TextView) findViewById(R.id.txtvTitle); - txtvFeed = (TextView) findViewById(R.id.txtvFeed); - butNavLeft = (ImageButton) findViewById(R.id.butNavLeft); - butNavRight = (ImageButton) findViewById(R.id.butNavRight); - butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed); + butNavRight.setImageDrawable(drawables.getDrawable(1)); + butNavRight.setContentDescription(getString(buttonTexts[1])); - butNavLeft.setOnClickListener(new OnClickListener() { + break; + case POS_DESCR: + butNavLeft.setScaleType(ScaleType.CENTER_CROP); + butNavLeft.post(new Runnable() { - @Override - public void onClick(View v) { - if (currentlyShownFragment == null - || currentlyShownPosition == POS_DESCR) { - switchToFragment(POS_COVER); - } else if (currentlyShownPosition == POS_COVER) { - switchToFragment(POS_DESCR); - } else if (currentlyShownPosition == POS_CHAPTERS) { - switchToFragment(POS_COVER); - } - } - }); + @Override + public void run() { + ImageLoader.getInstance().loadThumbnailBitmap(media, + butNavLeft); + } + }); + butNavLeft.setContentDescription(getString(buttonTexts[2])); - butNavRight.setOnClickListener(new OnClickListener() { + butNavRight.setImageDrawable(drawables.getDrawable(1)); + butNavRight.setContentDescription(getString(buttonTexts[1])); + break; + case POS_CHAPTERS: + butNavLeft.setScaleType(ScaleType.CENTER_CROP); + butNavLeft.post(new Runnable() { - @Override - public void onClick(View v) { - if (currentlyShownPosition == POS_CHAPTERS) { - switchToFragment(POS_DESCR); - } else { - switchToFragment(POS_CHAPTERS); - } - } - }); + @Override + public void run() { + ImageLoader.getInstance().loadThumbnailBitmap(media, + butNavLeft); + } + }); + butNavLeft.setContentDescription(getString(buttonTexts[2])); - butPlaybackSpeed.setOnClickListener(new OnClickListener() { + butNavRight.setImageDrawable(drawables.getDrawable(0)); + butNavRight.setContentDescription(getString(buttonTexts[0])); + break; + } + } + } + + @Override + protected void setupGUI() { + super.setupGUI(); + resetFragmentView(); + txtvTitle = (TextView) findViewById(R.id.txtvTitle); + txtvFeed = (TextView) findViewById(R.id.txtvFeed); + butNavLeft = (ImageButton) findViewById(R.id.butNavLeft); + butNavRight = (ImageButton) findViewById(R.id.butNavRight); + butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed); + + butNavLeft.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - if (controller != null && controller.canSetPlaybackSpeed()) { - String[] availableSpeeds = UserPreferences - .getPlaybackSpeedArray(); - String currentSpeed = UserPreferences.getPlaybackSpeed(); - - // Provide initial value in case the speed list has changed - // out from under us - // and our current speed isn't in the new list - String newSpeed; - if (availableSpeeds.length > 0) { - newSpeed = availableSpeeds[0]; - } else { - newSpeed = "1.0"; - } - - for (int i = 0; i < availableSpeeds.length; i++) { - if (availableSpeeds[i].equals(currentSpeed)) { - if (i == availableSpeeds.length - 1) { - newSpeed = availableSpeeds[0]; - } else { - newSpeed = availableSpeeds[i + 1]; - } - break; - } - } - UserPreferences.setPlaybackSpeed(newSpeed); - controller.setPlaybackSpeed(Float.parseFloat(newSpeed)); + if (currentlyShownFragment == null + || currentlyShownPosition == POS_DESCR) { + switchToFragment(POS_COVER); + } else if (currentlyShownPosition == POS_COVER) { + switchToFragment(POS_DESCR); + } else if (currentlyShownPosition == POS_CHAPTERS) { + switchToFragment(POS_COVER); } } }); - butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() { + butNavRight.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (currentlyShownPosition == POS_CHAPTERS) { + switchToFragment(POS_DESCR); + } else { + switchToFragment(POS_CHAPTERS); + } + } + }); + + butPlaybackSpeed.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (controller != null && controller.canSetPlaybackSpeed()) { + String[] availableSpeeds = UserPreferences + .getPlaybackSpeedArray(); + String currentSpeed = UserPreferences.getPlaybackSpeed(); + + // Provide initial value in case the speed list has changed + // out from under us + // and our current speed isn't in the new list + String newSpeed; + if (availableSpeeds.length > 0) { + newSpeed = availableSpeeds[0]; + } else { + newSpeed = "1.0"; + } + + for (int i = 0; i < availableSpeeds.length; i++) { + if (availableSpeeds[i].equals(currentSpeed)) { + if (i == availableSpeeds.length - 1) { + newSpeed = availableSpeeds[0]; + } else { + newSpeed = availableSpeeds[i + 1]; + } + break; + } + } + UserPreferences.setPlaybackSpeed(newSpeed); + controller.setPlaybackSpeed(Float.parseFloat(newSpeed)); + } + } + }); + + butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { VariableSpeedDialog.showDialog(AudioplayerActivity.this); return true; } }); - } + } - @Override - protected void onPlaybackSpeedChange() { - super.onPlaybackSpeedChange(); - updateButPlaybackSpeed(); - } + @Override + protected void onPlaybackSpeedChange() { + super.onPlaybackSpeedChange(); + updateButPlaybackSpeed(); + } - private void updateButPlaybackSpeed() { - if (controller == null - || (controller.getCurrentPlaybackSpeedMultiplier() == -1)) { - butPlaybackSpeed.setVisibility(View.GONE); - } else { - butPlaybackSpeed.setVisibility(View.VISIBLE); - butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed()); - } - } + private void updateButPlaybackSpeed() { + if (controller == null + || (controller.getCurrentPlaybackSpeedMultiplier() == -1)) { + butPlaybackSpeed.setVisibility(View.GONE); + } else { + butPlaybackSpeed.setVisibility(View.VISIBLE); + butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed()); + } + } - @Override - protected void onPositionObserverUpdate() { - super.onPositionObserverUpdate(); - notifyMediaPositionChanged(); - } + @Override + protected void onPositionObserverUpdate() { + super.onPositionObserverUpdate(); + notifyMediaPositionChanged(); + } - @Override - protected void loadMediaInfo() { - super.loadMediaInfo(); - final Playable media = controller.getMedia(); - if (media != null) { - txtvTitle.setText(media.getEpisodeTitle()); - txtvFeed.setText(media.getFeedTitle()); - if (media.getChapters() != null) { - butNavRight.setVisibility(View.VISIBLE); - } else { - butNavRight.setVisibility(View.GONE); - } + @Override + protected boolean loadMediaInfo() { + if (!super.loadMediaInfo()) { + return false; + } + final Playable media = controller.getMedia(); + if (media == null) { + return false; + } + txtvTitle.setText(media.getEpisodeTitle()); + txtvFeed.setText(media.getFeedTitle()); + if (media.getChapters() != null) { + butNavRight.setVisibility(View.VISIBLE); + } else { + butNavRight.setVisibility(View.GONE); + } - } - if (currentlyShownPosition == -1) { - if (!restoreFromPreferences()) { - switchToFragment(POS_COVER); - } - } - if (currentlyShownFragment instanceof AudioplayerContentFragment) { - ((AudioplayerContentFragment) currentlyShownFragment) - .onDataSetChanged(media); - } - updateButPlaybackSpeed(); - } - public void notifyMediaPositionChanged() { - if (chapterFragment != null) { - ArrayAdapter adapter = (ArrayAdapter) chapterFragment - .getListAdapter(); - adapter.notifyDataSetChanged(); - } - } + if (currentlyShownPosition == -1) { + if (!restoreFromPreferences()) { + switchToFragment(POS_COVER); + } + } + if (currentlyShownFragment instanceof AudioplayerContentFragment) { + ((AudioplayerContentFragment) currentlyShownFragment) + .onDataSetChanged(media); + } + updateButPlaybackSpeed(); + return true; + } - @Override - protected void onReloadNotification(int notificationCode) { - if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { - if (AppConfig.DEBUG) - Log.d(TAG, - "ReloadNotification received, switching to Videoplayer now"); - startActivity(new Intent(this, VideoplayerActivity.class)); + public void notifyMediaPositionChanged() { + if (chapterFragment != null) { + ArrayAdapter adapter = (ArrayAdapter) chapterFragment + .getListAdapter(); + adapter.notifyDataSetChanged(); + } + } - } - } + @Override + protected void onReloadNotification(int notificationCode) { + if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { + if (AppConfig.DEBUG) + Log.d(TAG, + "ReloadNotification received, switching to Videoplayer now"); + finish(); + startActivity(new Intent(this, VideoplayerActivity.class)); - @Override - protected void onBufferStart() { - postStatusMsg(R.string.player_buffering_msg); - } + } + } - @Override - protected void onBufferEnd() { - clearStatusMsg(); - } + @Override + protected void onBufferStart() { + postStatusMsg(R.string.player_buffering_msg); + } - public interface AudioplayerContentFragment { - public void onDataSetChanged(Playable media); - } + @Override + protected void onBufferEnd() { + clearStatusMsg(); + } - @Override - protected int getContentViewResourceId() { - return R.layout.audioplayer_activity; - } + public interface AudioplayerContentFragment { + public void onDataSetChanged(Playable media); + } + + @Override + protected int getContentViewResourceId() { + return R.layout.audioplayer_activity; + } } diff --git a/src/de/danoeh/antennapod/activity/DownloadActivity.java b/src/de/danoeh/antennapod/activity/DownloadActivity.java index 51491a286..f5986baf5 100644 --- a/src/de/danoeh/antennapod/activity/DownloadActivity.java +++ b/src/de/danoeh/antennapod/activity/DownloadActivity.java @@ -1,18 +1,9 @@ package de.danoeh.antennapod.activity; -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; import android.content.res.TypedArray; -import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; -import android.os.IBinder; -import android.support.v4.app.NavUtils; +import android.os.Handler; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.ActionBarActivity; import android.support.v7.view.ActionMode; @@ -22,17 +13,18 @@ import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemLongClickListener; - import android.widget.ListView; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadlistAdapter; +import de.danoeh.antennapod.asynctask.DownloadObserver; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.download.DownloadRequest; -import de.danoeh.antennapod.service.download.DownloadService; +import de.danoeh.antennapod.service.download.Downloader; import de.danoeh.antennapod.storage.DownloadRequester; +import java.util.List; + /** * Shows all running downloads in a list. The list objects are DownloadStatus * objects created by a DownloadObserver. @@ -49,13 +41,10 @@ public class DownloadActivity extends ActionBarActivity implements private ActionMode mActionMode; private DownloadRequest selectedDownload; - private DownloadService downloadService = null; - boolean mIsBound; - - private AsyncTask contentRefresher; - private ListView listview; + private DownloadObserver downloadObserver; + @Override protected void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); @@ -68,22 +57,19 @@ public class DownloadActivity extends ActionBarActivity implements Log.d(TAG, "Creating Activity"); requester = DownloadRequester.getInstance(); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + downloadObserver = new DownloadObserver(this, new Handler(), observerCallback); } @Override protected void onPause() { super.onPause(); - unbindService(mConnection); - unregisterReceiver(contentChanged); + downloadObserver.onPause(); } @Override protected void onResume() { super.onResume(); - registerReceiver(contentChanged, new IntentFilter( - DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); - bindService(new Intent(this, DownloadService.class), mConnection, 0); - startContentRefresher(); + downloadObserver.onResume(); if (dla != null) { dla.notifyDataSetChanged(); } @@ -94,72 +80,8 @@ public class DownloadActivity extends ActionBarActivity implements super.onStop(); if (AppConfig.DEBUG) Log.d(TAG, "Stopping Activity"); - stopContentRefresher(); } - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceDisconnected(ComponentName className) { - downloadService = null; - mIsBound = false; - Log.i(TAG, "Closed connection with DownloadService."); - } - - public void onServiceConnected(ComponentName name, IBinder service) { - downloadService = ((DownloadService.LocalBinder) service) - .getService(); - mIsBound = true; - if (AppConfig.DEBUG) - Log.d(TAG, "Connection to service established"); - dla = new DownloadlistAdapter(DownloadActivity.this, 0, - downloadService.getDownloads()); - listview.setAdapter(dla); - dla.notifyDataSetChanged(); - } - }; - - @SuppressLint("NewApi") - private void startContentRefresher() { - if (contentRefresher != null) { - contentRefresher.cancel(true); - } - contentRefresher = new AsyncTask() { - private static final int WAITING_INTERVAL = 1000; - - @Override - protected void onProgressUpdate(Void... values) { - super.onProgressUpdate(values); - if (dla != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing content automatically"); - dla.notifyDataSetChanged(); - } - } - - @Override - protected Void doInBackground(Void... params) { - while (!isCancelled()) { - try { - Thread.sleep(WAITING_INTERVAL); - publishProgress(); - } catch (InterruptedException e) { - return null; - } - } - return null; - } - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - contentRefresher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - contentRefresher.execute(); - } - } - - private void stopContentRefresher() { - if (contentRefresher != null) { - contentRefresher.cancel(true); - } - } @Override protected void onPostCreate(Bundle savedInstanceState) { @@ -240,31 +162,29 @@ public class DownloadActivity extends ActionBarActivity implements return handled; } - private boolean actionModeDestroyWorkaround = false; // TODO remove this workaround - private boolean skipWorkAround = Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH; - @Override public void onDestroyActionMode(ActionMode mode) { - if (skipWorkAround || actionModeDestroyWorkaround) { - mActionMode = null; - selectedDownload = null; - dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE); - actionModeDestroyWorkaround = false; - } else { - actionModeDestroyWorkaround = true; - } + mActionMode = null; + selectedDownload = null; + dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE); } - private BroadcastReceiver contentChanged = new BroadcastReceiver() { + private DownloadObserver.Callback observerCallback = new DownloadObserver.Callback() { @Override - public void onReceive(Context context, Intent intent) { + public void onContentChanged() { if (dla != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing content"); dla.notifyDataSetChanged(); } } + + @Override + public void onDownloadDataAvailable(List downloaderList) { + dla = new DownloadlistAdapter(DownloadActivity.this, 0, + downloaderList); + listview.setAdapter(dla); + dla.notifyDataSetChanged(); + } }; } diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java index f373bc35b..9edb312de 100644 --- a/src/de/danoeh/antennapod/activity/MainActivity.java +++ b/src/de/danoeh/antennapod/activity/MainActivity.java @@ -29,7 +29,7 @@ import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.FeedlistFragment; import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.service.download.DownloadService; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBTasks; diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java index 748a049a6..27ac7afd8 100644 --- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -16,13 +16,14 @@ import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.dialog.TimeDialog; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; +import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.ShareUtils; import de.danoeh.antennapod.util.StorageUtils; @@ -35,427 +36,442 @@ import de.danoeh.antennapod.util.playback.PlaybackController; * files. */ public abstract class MediaplayerActivity extends ActionBarActivity - implements OnSeekBarChangeListener { - private static final String TAG = "MediaplayerActivity"; + implements OnSeekBarChangeListener { + private static final String TAG = "MediaplayerActivity"; - protected PlaybackController controller; + protected PlaybackController controller; - protected TextView txtvPosition; - protected TextView txtvLength; - protected SeekBar sbPosition; - protected ImageButton butPlay; - protected ImageButton butRev; - protected ImageButton butFF; + protected TextView txtvPosition; + protected TextView txtvLength; + protected SeekBar sbPosition; + protected ImageButton butPlay; + protected ImageButton butRev; + protected ImageButton butFF; - private PlaybackController newPlaybackController() { - return new PlaybackController(this, false) { + private PlaybackController newPlaybackController() { + return new PlaybackController(this, false) { - @Override - public void setupGUI() { - MediaplayerActivity.this.setupGUI(); - } + @Override + public void setupGUI() { + MediaplayerActivity.this.setupGUI(); + } - @Override - public void onPositionObserverUpdate() { - MediaplayerActivity.this.onPositionObserverUpdate(); - } + @Override + public void onPositionObserverUpdate() { + MediaplayerActivity.this.onPositionObserverUpdate(); + } - @Override - public void onBufferStart() { - MediaplayerActivity.this.onBufferStart(); - } + @Override + public void onBufferStart() { + MediaplayerActivity.this.onBufferStart(); + } - @Override - public void onBufferEnd() { - MediaplayerActivity.this.onBufferEnd(); - } + @Override + public void onBufferEnd() { + MediaplayerActivity.this.onBufferEnd(); + } - @Override - public void onBufferUpdate(float progress) { - MediaplayerActivity.this.onBufferUpdate(progress); - } + @Override + public void onBufferUpdate(float progress) { + MediaplayerActivity.this.onBufferUpdate(progress); + } - @Override - public void handleError(int code) { - MediaplayerActivity.this.handleError(code); - } + @Override + public void handleError(int code) { + MediaplayerActivity.this.handleError(code); + } - @Override - public void onReloadNotification(int code) { - MediaplayerActivity.this.onReloadNotification(code); - } + @Override + public void onReloadNotification(int code) { + MediaplayerActivity.this.onReloadNotification(code); + } - @Override - public void onSleepTimerUpdate() { - supportInvalidateOptionsMenu(); - } + @Override + public void onSleepTimerUpdate() { + supportInvalidateOptionsMenu(); + } - @Override - public ImageButton getPlayButton() { - return butPlay; - } + @Override + public ImageButton getPlayButton() { + return butPlay; + } - @Override - public void postStatusMsg(int msg) { - MediaplayerActivity.this.postStatusMsg(msg); - } + @Override + public void postStatusMsg(int msg) { + MediaplayerActivity.this.postStatusMsg(msg); + } - @Override - public void clearStatusMsg() { - MediaplayerActivity.this.clearStatusMsg(); - } + @Override + public void clearStatusMsg() { + MediaplayerActivity.this.clearStatusMsg(); + } - @Override - public void loadMediaInfo() { - MediaplayerActivity.this.loadMediaInfo(); - } + @Override + public boolean loadMediaInfo() { + return MediaplayerActivity.this.loadMediaInfo(); + } - @Override - public void onAwaitingVideoSurface() { - MediaplayerActivity.this.onAwaitingVideoSurface(); - } + @Override + public void onAwaitingVideoSurface() { + MediaplayerActivity.this.onAwaitingVideoSurface(); + } - @Override - public void onServiceQueried() { - MediaplayerActivity.this.onServiceQueried(); - } + @Override + public void onServiceQueried() { + MediaplayerActivity.this.onServiceQueried(); + } - @Override - public void onShutdownNotification() { - finish(); - } + @Override + public void onShutdownNotification() { + finish(); + } - @Override - public void onPlaybackEnd() { - finish(); - } + @Override + public void onPlaybackEnd() { + finish(); + } - @Override - public void onPlaybackSpeedChange() { - MediaplayerActivity.this.onPlaybackSpeedChange(); - } - }; + @Override + public void onPlaybackSpeedChange() { + MediaplayerActivity.this.onPlaybackSpeedChange(); + } + }; - } + } - protected void onPlaybackSpeedChange() { + protected void onPlaybackSpeedChange() { - } + } - protected void onServiceQueried() { - supportInvalidateOptionsMenu(); - } + protected void onServiceQueried() { + supportInvalidateOptionsMenu(); + } - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - if (AppConfig.DEBUG) - Log.d(TAG, "Creating Activity"); - StorageUtils.checkStorageAvailability(this); + protected void chooseTheme() { + setTheme(UserPreferences.getTheme()); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + chooseTheme(); + super.onCreate(savedInstanceState); + if (AppConfig.DEBUG) + Log.d(TAG, "Creating Activity"); + StorageUtils.checkStorageAvailability(this); setVolumeControlStream(AudioManager.STREAM_MUSIC); orientation = getResources().getConfiguration().orientation; - getWindow().setFormat(PixelFormat.TRANSPARENT); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } + getWindow().setFormat(PixelFormat.TRANSPARENT); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } - @Override - protected void onPause() { - super.onPause(); - controller.reinitServiceIfPaused(); - controller.pause(); - } + @Override + protected void onPause() { + super.onPause(); + controller.reinitServiceIfPaused(); + controller.pause(); + } - /** - * Should be used to switch to another player activity if the mime type is - * not the correct one for the current activity. - */ - protected abstract void onReloadNotification(int notificationCode); + /** + * Should be used to switch to another player activity if the mime type is + * not the correct one for the current activity. + */ + protected abstract void onReloadNotification(int notificationCode); - /** - * Should be used to inform the user that the PlaybackService is currently - * buffering. - */ - protected abstract void onBufferStart(); + /** + * Should be used to inform the user that the PlaybackService is currently + * buffering. + */ + protected abstract void onBufferStart(); - /** - * Should be used to hide the view that was showing the 'buffering'-message. - */ - protected abstract void onBufferEnd(); + /** + * Should be used to hide the view that was showing the 'buffering'-message. + */ + protected abstract void onBufferEnd(); - protected void onBufferUpdate(float progress) { - if (sbPosition != null) { - sbPosition.setSecondaryProgress((int) progress - * sbPosition.getMax()); - } - } + protected void onBufferUpdate(float progress) { + if (sbPosition != null) { + sbPosition.setSecondaryProgress((int) progress + * sbPosition.getMax()); + } + } - /** Current screen orientation. */ - protected int orientation; + /** + * Current screen orientation. + */ + protected int orientation; - @Override - protected void onStart() { - super.onStart(); - if (controller != null) { - controller.release(); - } - controller = newPlaybackController(); - } + @Override + protected void onStart() { + super.onStart(); + if (controller != null) { + controller.release(); + } + controller = newPlaybackController(); + } - @Override - protected void onStop() { - super.onStop(); - if (AppConfig.DEBUG) - Log.d(TAG, "Activity stopped"); - if (controller != null) { - controller.release(); - } - } + @Override + protected void onStop() { + super.onStop(); + if (AppConfig.DEBUG) + Log.d(TAG, "Activity stopped"); + if (controller != null) { + controller.release(); + } + } - @Override - protected void onDestroy() { - super.onDestroy(); - if (AppConfig.DEBUG) - Log.d(TAG, "Activity destroyed"); - } + @Override + protected void onDestroy() { + super.onDestroy(); + if (AppConfig.DEBUG) + Log.d(TAG, "Activity destroyed"); + } - @Override - public boolean onCreateOptionsMenu(Menu menu) { + @Override + public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.mediaplayer, menu); - return true; - } + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mediaplayer, menu); + return true; + } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { + @Override + public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - Playable media = controller.getMedia(); + Playable media = controller.getMedia(); - menu.findItem(R.id.support_item).setVisible( - media != null && media.getPaymentLink() != null); - menu.findItem(R.id.share_link_item).setVisible( - media != null && media.getWebsiteLink() != null); - menu.findItem(R.id.visit_website_item).setVisible( - media != null && media.getWebsiteLink() != null); - menu.findItem(R.id.skip_episode_item).setVisible(media != null); - boolean sleepTimerSet = controller.sleepTimerActive(); - boolean sleepTimerNotSet = controller.sleepTimerNotActive(); - menu.findItem(R.id.set_sleeptimer_item).setVisible(sleepTimerNotSet); - menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerSet); - return true; - } + menu.findItem(R.id.support_item).setVisible( + media != null && media.getPaymentLink() != null && + (media instanceof FeedMedia) && + ((FeedMedia) media).getItem().getFlattrStatus().flattrable()); + menu.findItem(R.id.share_link_item).setVisible( + media != null && media.getWebsiteLink() != null); + menu.findItem(R.id.visit_website_item).setVisible( + media != null && media.getWebsiteLink() != null); + menu.findItem(R.id.skip_episode_item).setVisible(media != null); + boolean sleepTimerSet = controller.sleepTimerActive(); + boolean sleepTimerNotSet = controller.sleepTimerNotActive(); + menu.findItem(R.id.set_sleeptimer_item).setVisible(sleepTimerNotSet); + menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerSet); + return true; + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - Playable media = controller.getMedia(); - if (item.getItemId() == android.R.id.home) { - Intent intent = new Intent(MediaplayerActivity.this, - MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - return true; - } else if (media != null) { - switch (item.getItemId()) { - case R.id.disable_sleeptimer_item: - if (controller.serviceAvailable()) { - AlertDialog.Builder stDialog = new AlertDialog.Builder(this); - stDialog.setTitle(R.string.sleep_timer_label); - stDialog.setMessage(getString(R.string.time_left_label) - + Converter.getDurationStringLong((int) controller - .getSleepTimerTimeLeft())); - stDialog.setPositiveButton( - R.string.disable_sleeptimer_label, - new DialogInterface.OnClickListener() { + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Playable media = controller.getMedia(); + if (item.getItemId() == android.R.id.home) { + Intent intent = new Intent(MediaplayerActivity.this, + MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } else if (media != null) { + switch (item.getItemId()) { + case R.id.disable_sleeptimer_item: + if (controller.serviceAvailable()) { + AlertDialog.Builder stDialog = new AlertDialog.Builder(this); + stDialog.setTitle(R.string.sleep_timer_label); + stDialog.setMessage(getString(R.string.time_left_label) + + Converter.getDurationStringLong((int) controller + .getSleepTimerTimeLeft())); + stDialog.setPositiveButton( + R.string.disable_sleeptimer_label, + new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - dialog.dismiss(); - controller.disableSleepTimer(); - } - }); - stDialog.setNegativeButton(R.string.cancel_label, - new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + controller.disableSleepTimer(); + } + }); + stDialog.setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - dialog.dismiss(); - } - }); - stDialog.create().show(); - } - break; - case R.id.set_sleeptimer_item: - if (controller.serviceAvailable()) { - TimeDialog td = new TimeDialog(this, - R.string.set_sleeptimer_label, - R.string.set_sleeptimer_label) { + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + } + }); + stDialog.create().show(); + } + break; + case R.id.set_sleeptimer_item: + if (controller.serviceAvailable()) { + TimeDialog td = new TimeDialog(this, + R.string.set_sleeptimer_label, + R.string.set_sleeptimer_label) { - @Override - public void onTimeEntered(long millis) { - controller.setSleepTimer(millis); - } - }; - td.show(); - break; + @Override + public void onTimeEntered(long millis) { + controller.setSleepTimer(millis); + } + }; + td.show(); + break; - } - case R.id.visit_website_item: - Uri uri = Uri.parse(media.getWebsiteLink()); - startActivity(new Intent(Intent.ACTION_VIEW, uri)); - break; - case R.id.support_item: - new FlattrClickWorker(this, media.getPaymentLink()) - .executeAsync(); - break; - case R.id.share_link_item: - ShareUtils.shareLink(this, media.getWebsiteLink()); - break; - case R.id.skip_episode_item: - sendBroadcast(new Intent( - PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); - break; - default: - return false; + } + case R.id.visit_website_item: + Uri uri = Uri.parse(media.getWebsiteLink()); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + if (media instanceof FeedMedia) { + FeedItem feedItem = ((FeedMedia) media).getItem(); + DBTasks.flattrItemIfLoggedIn(this, feedItem); + } + break; + case R.id.share_link_item: + ShareUtils.shareLink(this, media.getWebsiteLink()); + break; + case R.id.skip_episode_item: + sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + default: + return false; - } - return true; - } else { - return false; - } - } + } + return true; + } else { + return false; + } + } - @Override - protected void onResume() { - super.onResume(); - if (AppConfig.DEBUG) - Log.d(TAG, "Resuming Activity"); - StorageUtils.checkStorageAvailability(this); - controller.init(); - } + @Override + protected void onResume() { + super.onResume(); + if (AppConfig.DEBUG) + Log.d(TAG, "Resuming Activity"); + StorageUtils.checkStorageAvailability(this); + controller.init(); + } - /** - * Called by 'handleStatus()' when the PlaybackService is in the - * AWAITING_VIDEO_SURFACE state. - */ - protected abstract void onAwaitingVideoSurface(); + /** + * Called by 'handleStatus()' when the PlaybackService is waiting for + * a video surface. + */ + protected abstract void onAwaitingVideoSurface(); - protected abstract void postStatusMsg(int resId); + protected abstract void postStatusMsg(int resId); - protected abstract void clearStatusMsg(); + protected abstract void clearStatusMsg(); - protected void onPositionObserverUpdate() { - if (controller != null) { - int currentPosition = controller.getPosition(); - int duration = controller.getDuration(); - if (currentPosition != PlaybackService.INVALID_TIME - && duration != PlaybackService.INVALID_TIME - && controller.getMedia() != null) { - controller.getMedia().setPosition(currentPosition); - txtvPosition.setText(Converter - .getDurationStringLong(currentPosition)); - txtvLength.setText(Converter.getDurationStringLong(duration)); - updateProgressbarPosition(currentPosition, duration); - } else { - Log.w(TAG, - "Could not react to position observer update because of invalid time"); - } - } - } + protected void onPositionObserverUpdate() { + if (controller != null) { + int currentPosition = controller.getPosition(); + int duration = controller.getDuration(); + if (currentPosition != PlaybackService.INVALID_TIME + && duration != PlaybackService.INVALID_TIME + && controller.getMedia() != null) { + txtvPosition.setText(Converter + .getDurationStringLong(currentPosition)); + txtvLength.setText(Converter.getDurationStringLong(duration)); + updateProgressbarPosition(currentPosition, duration); + } else { + Log.w(TAG, + "Could not react to position observer update because of invalid time"); + } + } + } - private void updateProgressbarPosition(int position, int duration) { - if (AppConfig.DEBUG) - Log.d(TAG, "Updating progressbar info"); - float progress = ((float) position) / duration; - sbPosition.setProgress((int) (progress * sbPosition.getMax())); - } + private void updateProgressbarPosition(int position, int duration) { + if (AppConfig.DEBUG) + Log.d(TAG, "Updating progressbar info"); + float progress = ((float) position) / duration; + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } - /** - * Load information about the media that is going to be played or currently - * being played. This method will be called when the activity is connected - * to the PlaybackService to ensure that the activity has the right - * FeedMedia object. - */ - protected void loadMediaInfo() { - if (AppConfig.DEBUG) - Log.d(TAG, "Loading media info"); - Playable media = controller.getMedia(); - if (media != null) { - txtvPosition.setText(Converter.getDurationStringLong((media - .getPosition()))); + /** + * Load information about the media that is going to be played or currently + * being played. This method will be called when the activity is connected + * to the PlaybackService to ensure that the activity has the right + * FeedMedia object. + */ + protected boolean loadMediaInfo() { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading media info"); + Playable media = controller.getMedia(); + if (media != null) { + txtvPosition.setText(Converter.getDurationStringLong((media + .getPosition()))); - if (media.getDuration() != 0) { - txtvLength.setText(Converter.getDurationStringLong(media - .getDuration())); - float progress = ((float) media.getPosition()) - / media.getDuration(); - sbPosition.setProgress((int) (progress * sbPosition.getMax())); - } - } - } + if (media.getDuration() != 0) { + txtvLength.setText(Converter.getDurationStringLong(media + .getDuration())); + float progress = ((float) media.getPosition()) + / media.getDuration(); + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } + return true; + } else { + return false; + } + } - protected void setupGUI() { - setContentView(getContentViewResourceId()); - sbPosition = (SeekBar) findViewById(R.id.sbPosition); - txtvPosition = (TextView) findViewById(R.id.txtvPosition); - txtvLength = (TextView) findViewById(R.id.txtvLength); - butPlay = (ImageButton) findViewById(R.id.butPlay); - butRev = (ImageButton) findViewById(R.id.butRev); - butFF = (ImageButton) findViewById(R.id.butFF); + protected void setupGUI() { + setContentView(getContentViewResourceId()); + sbPosition = (SeekBar) findViewById(R.id.sbPosition); + txtvPosition = (TextView) findViewById(R.id.txtvPosition); + txtvLength = (TextView) findViewById(R.id.txtvLength); + butPlay = (ImageButton) findViewById(R.id.butPlay); + butRev = (ImageButton) findViewById(R.id.butRev); + butFF = (ImageButton) findViewById(R.id.butFF); - // SEEKBAR SETUP + // SEEKBAR SETUP - sbPosition.setOnSeekBarChangeListener(this); + sbPosition.setOnSeekBarChangeListener(this); - // BUTTON SETUP + // BUTTON SETUP - butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); + butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); - butFF.setOnClickListener(controller.newOnFFButtonClickListener()); + if (butFF != null) { + butFF.setOnClickListener(controller.newOnFFButtonClickListener()); + } + if (butRev != null) { + butRev.setOnClickListener(controller.newOnRevButtonClickListener()); + } - butRev.setOnClickListener(controller.newOnRevButtonClickListener()); + } - } + protected abstract int getContentViewResourceId(); - protected abstract int getContentViewResourceId(); + void handleError(int errorCode) { + final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this); + errorDialog.setTitle(R.string.error_label); + errorDialog + .setMessage(MediaPlayerError.getErrorString(this, errorCode)); + errorDialog.setNeutralButton("OK", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + finish(); + } + }); + errorDialog.create().show(); + } - void handleError(int errorCode) { - final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this); - errorDialog.setTitle(R.string.error_label); - errorDialog - .setMessage(MediaPlayerError.getErrorString(this, errorCode)); - errorDialog.setNeutralButton("OK", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - finish(); - } - }); - errorDialog.create().show(); - } + float prog; - float prog; + @Override + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromUser) { + prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, + txtvPosition); + } - @Override - public void onProgressChanged(SeekBar seekBar, int progress, - boolean fromUser) { - prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, - txtvPosition); - } + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + controller.onSeekBarStartTrackingTouch(seekBar); + } - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - controller.onSeekBarStartTrackingTouch(seekBar); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - controller.onSeekBarStopTrackingTouch(seekBar, prog); - } + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + controller.onSeekBarStopTrackingTouch(seekBar, prog); + } } diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java index e6fcf5306..4a8dc1882 100644 --- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -28,7 +28,9 @@ import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.preferences.GpodnetPreferences; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import de.danoeh.antennapod.util.flattr.FlattrUtils; +import de.danoeh.antennapod.util.flattr.SimpleFlattrThing; import java.io.File; import java.util.ArrayList; @@ -44,6 +46,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { private static final String PREF_FLATTR_THIS_APP = "prefFlattrThisApp"; private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate"; private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess"; + private static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; private static final String PREF_OPML_EXPORT = "prefOpmlExport"; private static final String PREF_ABOUT = "prefAbout"; private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; @@ -78,7 +81,11 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { @Override public boolean onPreferenceClick(Preference preference) { new FlattrClickWorker(PreferenceActivity.this, - FlattrUtils.APP_URL).executeAsync(); + new SimpleFlattrThing(PreferenceActivity.this.getString(R.string.app_name), + FlattrUtils.APP_URL, + new FlattrStatus(FlattrStatus.STATUS_QUEUE) + ) + ).executeAsync(); return true; } @@ -297,6 +304,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); + findPreference(PREF_AUTO_FLATTR).setEnabled(hasFlattrToken); findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) .setEnabled(UserPreferences.isEnableAutodownload()); diff --git a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java index 01841f099..f323cb681 100644 --- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -2,289 +2,338 @@ package de.danoeh.antennapod.activity; import android.annotation.SuppressLint; import android.content.Intent; +import android.graphics.drawable.ColorDrawable; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.util.Pair; import android.view.*; +import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.SeekBar; -import android.widget.VideoView; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; -import de.danoeh.antennapod.service.PlayerStatus; +import de.danoeh.antennapod.service.playback.PlaybackService; +import de.danoeh.antennapod.service.playback.PlayerStatus; import de.danoeh.antennapod.util.playback.ExternalMedia; import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.view.AspectRatioVideoView; -/** Activity for playing audio files. */ -public class VideoplayerActivity extends MediaplayerActivity implements - SurfaceHolder.Callback { - private static final String TAG = "VideoplayerActivity"; +/** + * Activity for playing video files. + */ +public class VideoplayerActivity extends MediaplayerActivity { + private static final String TAG = "VideoplayerActivity"; - /** True if video controls are currently visible. */ - private boolean videoControlsShowing = true; - private boolean videoSurfaceCreated = false; - private VideoControlsHider videoControlsToggler; + /** + * True if video controls are currently visible. + */ + private boolean videoControlsShowing = true; + private boolean videoSurfaceCreated = false; + private VideoControlsHider videoControlsToggler; - private LinearLayout videoOverlay; - private VideoView videoview; - private ProgressBar progressIndicator; + private LinearLayout videoOverlay; + private AspectRatioVideoView videoview; + private ProgressBar progressIndicator; - @Override - protected void onCreate(Bundle savedInstanceState) { - requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); - setTheme(UserPreferences.getTheme()); + @Override + protected void chooseTheme() { + setTheme(R.style.Theme_AntennaPod_Dark); + } - super.onCreate(savedInstanceState); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + if (Build.VERSION.SDK_INT >= 11) { + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + } + super.onCreate(savedInstanceState); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000)); + } - @Override - protected void onPause() { - super.onPause(); - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - } + @Override + protected void onPause() { + super.onPause(); + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { + controller.pause(); + } + } - @Override - protected void onResume() { - super.onResume(); - if (getIntent().getAction() != null - && getIntent().getAction().equals(Intent.ACTION_VIEW)) { - Intent intent = getIntent(); - if (AppConfig.DEBUG) - Log.d(TAG, "Received VIEW intent: " - + intent.getData().getPath()); - ExternalMedia media = new ExternalMedia(intent.getData().getPath(), - MediaType.VIDEO); - Intent launchIntent = new Intent(this, PlaybackService.class); - launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, - true); - launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); - launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, - true); - startService(launchIntent); - } - } + @Override + protected void onResume() { + super.onResume(); + if (getIntent().getAction() != null + && getIntent().getAction().equals(Intent.ACTION_VIEW)) { + Intent intent = getIntent(); + if (AppConfig.DEBUG) + Log.d(TAG, "Received VIEW intent: " + + intent.getData().getPath()); + ExternalMedia media = new ExternalMedia(intent.getData().getPath(), + MediaType.VIDEO); + Intent launchIntent = new Intent(this, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + true); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + startService(launchIntent); + } + } - @Override - protected void loadMediaInfo() { - super.loadMediaInfo(); - Playable media = controller.getMedia(); - if (media != null) { - getSupportActionBar().setSubtitle(media.getEpisodeTitle()); - getSupportActionBar().setTitle(media.getFeedTitle()); - } - } + @Override + protected boolean loadMediaInfo() { + if (!super.loadMediaInfo()) { + return false; + } + Playable media = controller.getMedia(); + if (media != null) { + getSupportActionBar().setSubtitle(media.getEpisodeTitle()); + getSupportActionBar().setTitle(media.getFeedTitle()); + return true; + } - @Override - protected void setupGUI() { - super.setupGUI(); - videoOverlay = (LinearLayout) findViewById(R.id.overlay); - videoview = (VideoView) findViewById(R.id.videoview); - progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator); - videoview.getHolder().addCallback(this); - videoview.setOnTouchListener(onVideoviewTouched); + return false; + } - setupVideoControlsToggler(); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - } + @Override + protected void setupGUI() { + super.setupGUI(); + videoOverlay = (LinearLayout) findViewById(R.id.overlay); + videoview = (AspectRatioVideoView) findViewById(R.id.videoview); + progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator); + videoview.getHolder().addCallback(surfaceHolderCallback); + videoview.setOnTouchListener(onVideoviewTouched); - @Override - protected void onAwaitingVideoSurface() { - if (videoSurfaceCreated) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Videosurface already created, setting videosurface now"); - controller.setVideoSurface(videoview.getHolder()); - } - } + setupVideoControlsToggler(); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } - @Override - protected void postStatusMsg(int resId) { - if (resId == R.string.player_preparing_msg) { - progressIndicator.setVisibility(View.VISIBLE); - } else { - progressIndicator.setVisibility(View.INVISIBLE); - } + @Override + protected void onAwaitingVideoSurface() { + if (videoSurfaceCreated) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Videosurface already created, setting videosurface now"); - } + Pair videoSize = controller.getVideoSize(); + if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { + if (AppConfig.DEBUG) Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); + videoview.setVideoSize(videoSize.first, videoSize.second); + } else { + Log.e(TAG, "Could not determine video size"); + } + controller.setVideoSurface(videoview.getHolder()); + } + } - @Override - protected void clearStatusMsg() { - progressIndicator.setVisibility(View.INVISIBLE); - } + @Override + protected void postStatusMsg(int resId) { + if (resId == R.string.player_preparing_msg) { + progressIndicator.setVisibility(View.VISIBLE); + } else { + progressIndicator.setVisibility(View.INVISIBLE); + } - View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() { + } - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - toggleVideoControlsVisibility(); - if (videoControlsShowing) { - setupVideoControlsToggler(); - } + @Override + protected void clearStatusMsg() { + progressIndicator.setVisibility(View.INVISIBLE); + } - return true; - } else { - return false; - } - } - }; + View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() { - @SuppressLint("NewApi") - void setupVideoControlsToggler() { - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - videoControlsToggler = new VideoControlsHider(); - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - videoControlsToggler - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - videoControlsToggler.execute(); - } - } + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + toggleVideoControlsVisibility(); + if (videoControlsShowing) { + setupVideoControlsToggler(); + } - private void toggleVideoControlsVisibility() { - if (videoControlsShowing) { - getSupportActionBar().hide(); - hideVideoControls(); - } else { - getSupportActionBar().show(); - showVideoControls(); - } - videoControlsShowing = !videoControlsShowing; - } + return true; + } else { + return false; + } + } + }; - /** Hides the videocontrols after a certain period of time. */ - public class VideoControlsHider extends AsyncTask { - @Override - protected void onCancelled() { - videoControlsToggler = null; - } + @SuppressLint("NewApi") + void setupVideoControlsToggler() { + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + videoControlsToggler = new VideoControlsHider(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + videoControlsToggler + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + videoControlsToggler.execute(); + } + } - @Override - protected void onPostExecute(Void result) { - videoControlsToggler = null; - } + private void toggleVideoControlsVisibility() { + if (videoControlsShowing) { + getSupportActionBar().hide(); + hideVideoControls(); + } else { + getSupportActionBar().show(); + showVideoControls(); + } + videoControlsShowing = !videoControlsShowing; + } - private static final int WAITING_INTERVALL = 5000; - private static final String TAG = "VideoControlsToggler"; + /** + * Hides the videocontrols after a certain period of time. + */ + public class VideoControlsHider extends AsyncTask { + @Override + protected void onCancelled() { + videoControlsToggler = null; + } - @Override - protected void onProgressUpdate(Void... values) { - if (videoControlsShowing) { - if (AppConfig.DEBUG) - Log.d(TAG, "Hiding video controls"); - getSupportActionBar().hide(); - hideVideoControls(); - videoControlsShowing = false; - } - } + @Override + protected void onPostExecute(Void result) { + videoControlsToggler = null; + } - @Override - protected Void doInBackground(Void... params) { - try { - Thread.sleep(WAITING_INTERVALL); - } catch (InterruptedException e) { - return null; - } - publishProgress(); - return null; - } + private static final int WAITING_INTERVALL = 5000; + private static final String TAG = "VideoControlsToggler"; - } + @Override + protected void onProgressUpdate(Void... values) { + if (videoControlsShowing) { + if (AppConfig.DEBUG) + Log.d(TAG, "Hiding video controls"); + getSupportActionBar().hide(); + hideVideoControls(); + videoControlsShowing = false; + } + } - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, - int height) { - holder.setFixedSize(width, height); - } + @Override + protected Void doInBackground(Void... params) { + try { + Thread.sleep(WAITING_INTERVALL); + } catch (InterruptedException e) { + return null; + } + publishProgress(); + return null; + } - @Override - public void surfaceCreated(SurfaceHolder holder) { - if (AppConfig.DEBUG) - Log.d(TAG, "Videoview holder created"); - videoSurfaceCreated = true; - if (controller.getStatus() == PlayerStatus.AWAITING_VIDEO_SURFACE) { - if (controller.serviceAvailable()) { - controller.setVideoSurface(holder); - } else { - Log.e(TAG, - "Could'nt attach surface to mediaplayer - reference to service was null"); - } - } + } - } + private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + holder.setFixedSize(width, height); + } - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - if (AppConfig.DEBUG) - Log.d(TAG, "Videosurface was destroyed"); - videoSurfaceCreated = false; - controller.notifyVideoSurfaceAbandoned(); - } + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (AppConfig.DEBUG) + Log.d(TAG, "Videoview holder created"); + videoSurfaceCreated = true; + if (controller.getStatus() == PlayerStatus.PLAYING) { + if (controller.serviceAvailable()) { + controller.setVideoSurface(holder); + } else { + Log.e(TAG, + "Could'nt attach surface to mediaplayer - reference to service was null"); + } + } - @Override - protected void onReloadNotification(int notificationCode) { - if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) { - if (AppConfig.DEBUG) - Log.d(TAG, - "ReloadNotification received, switching to Audioplayer now"); - startActivity(new Intent(this, AudioplayerActivity.class)); - } - } + } - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - super.onStartTrackingTouch(seekBar); - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - } + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (AppConfig.DEBUG) + Log.d(TAG, "Videosurface was destroyed"); + videoSurfaceCreated = false; + controller.notifyVideoSurfaceAbandoned(); + } + }; - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - setupVideoControlsToggler(); - } - @Override - protected void onBufferStart() { - progressIndicator.setVisibility(View.VISIBLE); - } + @Override + protected void onReloadNotification(int notificationCode) { + if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) { + if (AppConfig.DEBUG) + Log.d(TAG, + "ReloadNotification received, switching to Audioplayer now"); + finish(); + startActivity(new Intent(this, AudioplayerActivity.class)); + } + } - @Override - protected void onBufferEnd() { - progressIndicator.setVisibility(View.INVISIBLE); - } + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + super.onStartTrackingTouch(seekBar); + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + } - private void showVideoControls() { - videoOverlay.setVisibility(View.VISIBLE); - videoOverlay.startAnimation(AnimationUtils.loadAnimation(this, - R.anim.fade_in)); - } + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + setupVideoControlsToggler(); + } - private void hideVideoControls() { - videoOverlay.startAnimation(AnimationUtils.loadAnimation(this, - R.anim.fade_out)); - videoOverlay.setVisibility(View.GONE); - } + @Override + protected void onBufferStart() { + progressIndicator.setVisibility(View.VISIBLE); + } - @Override - protected int getContentViewResourceId() { - return R.layout.videoplayer_activity; - } + @Override + protected void onBufferEnd() { + progressIndicator.setVisibility(View.INVISIBLE); + } + + private void showVideoControls() { + videoOverlay.setVisibility(View.VISIBLE); + butPlay.setVisibility(View.VISIBLE); + final Animation animation = AnimationUtils.loadAnimation(this, + R.anim.fade_in); + if (animation != null) { + videoOverlay.startAnimation(animation); + butPlay.startAnimation(animation); + } + if (Build.VERSION.SDK_INT >= 14) { + videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + } + + private void hideVideoControls() { + final Animation animation = AnimationUtils.loadAnimation(this, + R.anim.fade_out); + if (animation != null) { + videoOverlay.startAnimation(animation); + butPlay.startAnimation(animation); + } + if (Build.VERSION.SDK_INT >= 14) { + videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } + videoOverlay.setVisibility(View.GONE); + butPlay.setVisibility(View.GONE); + } + + @Override + protected int getContentViewResourceId() { + return R.layout.videoplayer_activity; + } } diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java index d355a7826..e5a00923a 100644 --- a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -56,9 +56,9 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTheme(UserPreferences.getTheme()); setContentView(R.layout.gpodnetauth_activity); service = new GpodnetService(); diff --git a/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java index 2b49795c3..e384ecffc 100644 --- a/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java @@ -91,10 +91,12 @@ public class DefaultFeedItemlistAdapter extends BaseAdapter { MediaType mediaType = item.getMedia().getMediaType(); if (mediaType == MediaType.AUDIO) { holder.type.setImageDrawable(typeDrawables.getDrawable(0)); + holder.type.setContentDescription(context.getString(R.string.media_type_audio_label)); holder.type.setVisibility(View.VISIBLE); } else if (mediaType == MediaType.VIDEO) { holder.type.setImageDrawable(typeDrawables.getDrawable(1)); - holder.type.setVisibility(View.VISIBLE); + holder.type.setContentDescription(context.getString(R.string.media_type_video_label)); + holder.type.setVisibility(View.VISIBLE); } else { holder.type.setImageBitmap(null); holder.type.setVisibility(View.GONE); diff --git a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java index b00066eca..aed988b59 100644 --- a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java @@ -148,12 +148,14 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { TypedArray drawables = context.obtainStyledAttributes(new int[] { R.attr.av_download, R.attr.navigation_refresh }); + final int[] labels = new int[] {R.string.status_downloaded_label, R.string.downloading_label}; holder.lenSize.setVisibility(View.VISIBLE); if (!media.isDownloaded()) { if (DownloadRequester.getInstance().isDownloadingFile(media)) { holder.downloadStatus.setVisibility(View.VISIBLE); holder.downloadStatus.setImageDrawable(drawables .getDrawable(1)); + holder.downloadStatus.setContentDescription(context.getString(labels[1])); } else { holder.downloadStatus.setVisibility(View.INVISIBLE); } @@ -161,6 +163,7 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { holder.downloadStatus.setVisibility(View.VISIBLE); holder.downloadStatus .setImageDrawable(drawables.getDrawable(0)); + holder.downloadStatus.setContentDescription(context.getString(labels[0])); } } else { holder.downloadStatus.setVisibility(View.INVISIBLE); diff --git a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java index b8bec44c8..238ae29c6 100644 --- a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java @@ -176,12 +176,16 @@ public class InternalFeedItemlistAdapter extends DefaultFeedItemlistAdapter { TypedArray typeDrawables = getContext().obtainStyledAttributes( new int[] { R.attr.type_audio, R.attr.type_video }); + final int[] labels = new int[] {R.string.media_type_audio_label, R.string.media_type_video_label}; + MediaType mediaType = item.getMedia().getMediaType(); if (mediaType == MediaType.AUDIO) { holder.type.setImageDrawable(typeDrawables.getDrawable(0)); + holder.type.setContentDescription(getContext().getString(labels[0])); holder.type.setVisibility(View.VISIBLE); } else if (mediaType == MediaType.VIDEO) { holder.type.setImageDrawable(typeDrawables.getDrawable(1)); + holder.type.setContentDescription(getContext().getString(labels[1])); holder.type.setVisibility(View.VISIBLE); } else { holder.type.setImageBitmap(null); diff --git a/src/de/danoeh/antennapod/asynctask/DownloadObserver.java b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java new file mode 100644 index 000000000..26e405615 --- /dev/null +++ b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java @@ -0,0 +1,150 @@ +package de.danoeh.antennapod.asynctask; + +import android.app.Activity; +import android.content.*; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.service.download.DownloadService; +import de.danoeh.antennapod.service.download.Downloader; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provides access to the DownloadService's list of items that are currently being downloaded. + * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause() + * should be called in the activity's onResume() and onPause() methods + */ +public class DownloadObserver { + private static final String TAG = "DownloadObserver"; + + /** + * Time period between update notifications. + */ + public static final int WAITING_INTERVAL_MS = 1000; + + private final Activity activity; + private final Handler handler; + private final Callback callback; + + private DownloadService downloadService = null; + private AtomicBoolean mIsBound = new AtomicBoolean(false); + + private Thread refresherThread; + private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false); + + + /** + * Creates a new download observer. + * + * @param activity Used for registering receivers + * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread. + * @param callback Callback methods for posting content updates + * @throws java.lang.IllegalArgumentException if one of the arguments is null. + */ + public DownloadObserver(Activity activity, Handler handler, Callback callback) { + if (activity == null) throw new IllegalArgumentException("activity = null"); + if (handler == null) throw new IllegalArgumentException("handler = null"); + if (callback == null) throw new IllegalArgumentException("callback = null"); + + this.activity = activity; + this.handler = handler; + this.callback = callback; + } + + public void onResume() { + if (AppConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed"); + activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); + activity.bindService(new Intent(activity, DownloadService.class), mConnection, 0); + } + + public void onPause() { + if (AppConfig.DEBUG) Log.d(TAG, "DownloadObserver paused"); + activity.unregisterReceiver(contentChangedReceiver); + activity.unbindService(mConnection); + stopRefresher(); + } + + private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + callback.onContentChanged(); + startRefresher(); + } + }; + + public interface Callback { + void onContentChanged(); + + void onDownloadDataAvailable(List downloaderList); + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceDisconnected(ComponentName className) { + downloadService = null; + mIsBound.set(false); + stopRefresher(); + Log.i(TAG, "Closed connection with DownloadService."); + } + + public void onServiceConnected(ComponentName name, IBinder service) { + downloadService = ((DownloadService.LocalBinder) service) + .getService(); + mIsBound.set(true); + if (AppConfig.DEBUG) + Log.d(TAG, "Connection to service established"); + List downloaderList = downloadService.getDownloads(); + if (downloaderList != null && !downloaderList.isEmpty()) { + callback.onDownloadDataAvailable(downloaderList); + startRefresher(); + } + } + }; + + private void stopRefresher() { + if (refresherThread != null) { + refresherThread.interrupt(); + } + } + + private void startRefresher() { + if (refresherThread == null || refresherThread.isInterrupted()) { + refresherThread = new Thread(new RefresherThread()); + refresherThread.start(); + } + } + + private class RefresherThread implements Runnable { + + public void run() { + refresherThreadRunning.set(true); + while (!Thread.interrupted()) { + try { + Thread.sleep(WAITING_INTERVAL_MS); + } catch (InterruptedException e) { + Log.d(TAG, "Refresher thread was interrupted"); + } + if (mIsBound.get()) { + postUpdate(); + } + } + refresherThreadRunning.set(false); + } + + private void postUpdate() { + handler.post(new Runnable() { + @Override + public void run() { + callback.onContentChanged(); + List downloaderList = downloadService.getDownloads(); + if (downloaderList == null || downloaderList.isEmpty()) { + Thread.currentThread().interrupt(); + } + } + }); + } + } + +} diff --git a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java index 975aa5efe..3034bbaff 100644 --- a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java +++ b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java @@ -1,115 +1,308 @@ package de.danoeh.antennapod.asynctask; -import org.shredzone.flattr4j.exception.FlattrException; - import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; import android.app.ProgressDialog; import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.AsyncTask; +import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.flattr.FlattrThing; import de.danoeh.antennapod.util.flattr.FlattrUtils; -/** Performs a click action in a background thread. */ +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; -public class FlattrClickWorker extends AsyncTask { - protected static final String TAG = "FlattrClickWorker"; - protected Context context; - protected String url; - protected String errorMsg; - protected int exitCode; - protected ProgressDialog progDialog; +/** + * Performs a click action in a background thread. + */ - protected final static int SUCCESS = 0; - protected final static int NO_TOKEN = 1; - protected final static int FLATTR_ERROR = 2; +public class FlattrClickWorker extends AsyncTask { + protected static final String TAG = "FlattrClickWorker"; + protected Context context; - public FlattrClickWorker(Context context, String url) { - super(); - this.context = context; - this.url = url; - exitCode = SUCCESS; - errorMsg = ""; - } + private final int NOTIFICATION_ID = 4; - protected void onNoAccessToken() { - Log.w(TAG, "No access token was available"); - if (url.equals(FlattrUtils.APP_URL)) { - FlattrUtils.showNoTokenDialog(context, FlattrUtils.APP_LINK); - } else { - FlattrUtils.showNoTokenDialog(context, url); - } - } + protected String errorMsg; + protected int exitCode; + protected ArrayList flattrd; + protected ArrayList flattr_failed; - protected void onFlattrError() { - FlattrUtils.showErrorDialog(context, errorMsg); - } - protected void onSuccess() { - Toast toast = Toast.makeText(context.getApplicationContext(), - R.string.flattr_click_success, Toast.LENGTH_LONG); - toast.show(); - } + protected NotificationCompat.Builder notificationCompatBuilder; + private Notification.BigTextStyle notificationBuilder; + protected NotificationManager notificationManager; - protected void onSetupProgDialog() { - progDialog = new ProgressDialog(context); - progDialog.setMessage(context.getString(R.string.flattring_label)); - progDialog.setIndeterminate(true); - progDialog.setCancelable(false); - progDialog.show(); - } + protected ProgressDialog progDialog; - @Override - protected void onPostExecute(Void result) { - if (AppConfig.DEBUG) Log.d(TAG, "Exit code was " + exitCode); - if (progDialog != null) { - progDialog.dismiss(); - } - switch (exitCode) { - case NO_TOKEN: - onNoAccessToken(); - break; - case FLATTR_ERROR: - onFlattrError(); - break; - case SUCCESS: - onSuccess(); - break; - } - } + protected final static int EXIT_DEFAULT = 0; + protected final static int NO_TOKEN = 1; + protected final static int ENQUEUED = 2; + protected final static int NO_THINGS = 3; - @Override - protected void onPreExecute() { - onSetupProgDialog(); - } + public final static int ENQUEUE_ONLY = 1; + public final static int FLATTR_TOAST = 2; + public static final int FLATTR_NOTIFICATION = 3; - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) Log.d(TAG, "Starting background work"); - if (FlattrUtils.hasToken()) { - try { - FlattrUtils.clickUrl(context, url); - } catch (FlattrException e) { - e.printStackTrace(); - exitCode = FLATTR_ERROR; - errorMsg = e.getMessage(); - } - } else { - exitCode = NO_TOKEN; - } - return null; - } + private int run_mode = FLATTR_NOTIFICATION; + + private FlattrThing extra_flattr_thing; // additional urls to flattr that do *not* originate from the queue + + /** + * @param context + * @param run_mode can be one of ENQUEUE_ONLY, FLATTR_TOAST and FLATTR_NOTIFICATION + */ + public FlattrClickWorker(Context context, int run_mode) { + this(context); + this.run_mode = run_mode; + } + + public FlattrClickWorker(Context context) { + super(); + this.context = context; + exitCode = EXIT_DEFAULT; + + flattrd = new ArrayList(); + flattr_failed = new ArrayList(); + + errorMsg = ""; + } + + /* only used in PreferencesActivity for flattring antennapod itself, + * can't really enqueue this thing + */ + public FlattrClickWorker(Context context, FlattrThing thing) { + this(context); + extra_flattr_thing = thing; + run_mode = FLATTR_TOAST; + Log.d(TAG, "Going to flattr special thing that is not in the queue: " + thing.getTitle()); + } + + protected void onNoAccessToken() { + Log.w(TAG, "No access token was available"); + } + + protected void onFlattrError() { + FlattrUtils.showErrorDialog(context, errorMsg); + } + + protected void onFlattred() { + String notificationTitle = context.getString(R.string.flattrd_label); + String notificationText = "", notificationSubText = "", notificationBigText = ""; + + // text for successfully flattred items + if (flattrd.size() == 1) + notificationText = String.format(context.getString(R.string.flattr_click_success)); + else if (flattrd.size() > 1) // flattred pending items from queue + notificationText = String.format(context.getString(R.string.flattr_click_success_count, flattrd.size())); + + if (flattrd.size() > 0) { + String acc = ""; + for (String s : flattrd) + acc += s + '\n'; + acc = acc.substring(0, acc.length() - 2); + + notificationBigText = String.format(context.getString(R.string.flattr_click_success_queue), acc); + } + + // add text for failures + if (flattr_failed.size() > 0) { + notificationTitle = context.getString(R.string.flattrd_failed_label); + notificationText = String.format(context.getString(R.string.flattr_click_failure_count), flattr_failed.size()) + + " " + notificationText; + + notificationSubText = flattr_failed.get(0); + + String acc = ""; + for (String s : flattr_failed) + acc += s + '\n'; + acc = acc.substring(0, acc.length() - 2); + + notificationBigText = String.format(context.getString(R.string.flattr_click_failure), acc) + + "\n" + notificationBigText; + } + + Log.d(TAG, "Going to post notification: " + notificationBigText); + + notificationManager.cancel(NOTIFICATION_ID); + + if (run_mode == FLATTR_NOTIFICATION || flattr_failed.size() > 0) { + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder = new Notification.BigTextStyle( + new Notification.Builder(context) + .setOngoing(false) + .setContentTitle(notificationTitle) + .setContentText(notificationText) + .setSubText(notificationSubText) + .setSmallIcon(R.drawable.stat_notify_sync)) + .bigText(notificationText + "\n" + notificationBigText); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } else { + notificationCompatBuilder = new NotificationCompat.Builder(context) // need new notificationBuilder and cancel/renotify to get rid of progress bar + .setContentTitle(notificationTitle) + .setContentText(notificationText) + .setSubText(notificationBigText) + .setTicker(notificationTitle) + .setSmallIcon(R.drawable.stat_notify_sync) + .setOngoing(false); + notificationManager.notify(NOTIFICATION_ID, notificationCompatBuilder.build()); + } + } else if (run_mode == FLATTR_TOAST) { + Toast.makeText(context.getApplicationContext(), + notificationText, + Toast.LENGTH_LONG) + .show(); + } + } + + protected void onEnqueue() { + Toast.makeText(context.getApplicationContext(), + R.string.flattr_click_enqueued, + Toast.LENGTH_LONG) + .show(); + } + + protected void onSetupNotification() { + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder = new Notification.BigTextStyle( + new Notification.Builder(context) + .setContentTitle(context.getString(R.string.flattring_label)) + .setAutoCancel(true) + .setSmallIcon(R.drawable.stat_notify_sync) + .setProgress(0, 0, true) + .setOngoing(true)); + } else { + notificationCompatBuilder = new NotificationCompat.Builder(context) + .setContentTitle(context.getString(R.string.flattring_label)) + .setAutoCancel(true) + .setSmallIcon(R.drawable.stat_notify_sync) + .setProgress(0, 0, true) + .setOngoing(true); + } + + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + @Override + protected void onPostExecute(Void result) { + if (AppConfig.DEBUG) Log.d(TAG, "Exit code was " + exitCode); + + switch (exitCode) { + case NO_TOKEN: + notificationManager.cancel(NOTIFICATION_ID); + onNoAccessToken(); + break; + case ENQUEUED: + onEnqueue(); + break; + case EXIT_DEFAULT: + onFlattred(); + break; + case NO_THINGS: // FlattrClickWorker called automatically somewhere to empty flattr queue + notificationManager.cancel(NOTIFICATION_ID); + break; + } + } + + @Override + protected void onPreExecute() { + onSetupNotification(); + } + + private static boolean haveInternetAccess(Context context) { + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + return (networkInfo != null && networkInfo.isConnectedOrConnecting()); + } + + @Override + protected Void doInBackground(Void... params) { + if (AppConfig.DEBUG) Log.d(TAG, "Starting background work"); + + exitCode = EXIT_DEFAULT; + + if (!FlattrUtils.hasToken()) { + exitCode = NO_TOKEN; + } else if (DBReader.getFlattrQueueEmpty(context) && extra_flattr_thing == null) { + exitCode = NO_THINGS; + } else if (!haveInternetAccess(context) || run_mode == ENQUEUE_ONLY) { + exitCode = ENQUEUED; + } else { + List flattrList = DBReader.getFlattrQueue(context); + Log.d(TAG, "flattrQueue processing list with " + flattrList.size() + " items."); + + if (extra_flattr_thing != null) + flattrList.add(extra_flattr_thing); + + flattrd.ensureCapacity(flattrList.size()); + + for (FlattrThing thing : flattrList) { + try { + Log.d(TAG, "flattrQueue processing " + thing.getTitle() + " " + thing.getPaymentLink()); + publishProgress(String.format(context.getString(R.string.flattring_thing), thing.getTitle())); + + thing.getFlattrStatus().setUnflattred(); // pop from queue to prevent unflattrable things from getting stuck in flattr queue infinitely + + FlattrUtils.clickUrl(context, thing.getPaymentLink()); + flattrd.add(thing.getTitle()); + + thing.getFlattrStatus().setFlattred(); + } catch (Exception e) { + Log.d(TAG, "flattrQueue processing exception at item " + thing.getTitle() + " " + e.getMessage()); + flattr_failed.ensureCapacity(flattrList.size()); + flattr_failed.add(thing.getTitle() + ": " + e.getMessage()); + } + Log.d(TAG, "flattrQueue processing - going to write thing back to db with flattr_status " + Long.toString(thing.getFlattrStatus().toLong())); + DBWriter.setFlattredStatus(context, thing, false); + } + + } + + return null; + } + + @Override + protected void onProgressUpdate(String... names) { + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder.setBigContentTitle(names[0]); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } else { + notificationCompatBuilder.setContentText(names[0]); + notificationManager.notify(NOTIFICATION_ID, notificationCompatBuilder.build()); + } + } + + @SuppressLint("NewApi") + public void executeAsync() { + FlattrUtils.hasToken(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + + public void executeSync() { + class DirectExecutor implements Executor { + public void execute(Runnable r) { + r.run(); + } + } + FlattrUtils.hasToken(); + executeOnExecutor(new DirectExecutor()); + + } - @SuppressLint("NewApi") - public void executeAsync() { - FlattrUtils.hasToken(); - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(THREAD_POOL_EXECUTOR); - } else { - execute(); - } - } } diff --git a/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java b/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java new file mode 100644 index 000000000..4974c6b56 --- /dev/null +++ b/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.asynctask; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.flattr.FlattrUtils; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Fetch list of flattred things and flattr status in database in a background thread. + */ + +public class FlattrStatusFetcher extends Thread { + protected static final String TAG = "FlattrStatusFetcher"; + protected Context context; + + public FlattrStatusFetcher(Context context) { + super(); + this.context = context; + } + + @Override + public void run() { + if (AppConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status"); + + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + + try { + List flattredThings = FlattrUtils.retrieveFlattredThings(); + DBWriter.setFlattredStatus(context, flattredThings).get(); + } catch (FlattrException e) { + e.printStackTrace(); + Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + if (AppConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status"); + } +} diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java index a99213dc7..994446f43 100644 --- a/src/de/danoeh/antennapod/feed/Feed.java +++ b/src/de/danoeh/antennapod/feed/Feed.java @@ -10,13 +10,15 @@ import java.util.List; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.EpisodeFilter; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; /** * Data Object for a whole feed * * @author daniel */ -public class Feed extends FeedFile { +public class Feed extends FeedFile implements FlattrThing { public static final int FEEDFILETYPE_FEED = 0; public static final String TYPE_RSS2 = "rss"; public static final String TYPE_RSS091 = "rss"; @@ -43,6 +45,7 @@ public class Feed extends FeedFile { * Date of last refresh. */ private Date lastUpdate; + private FlattrStatus flattrStatus; private String paymentLink; /** * Feed type, for example RSS 2 or Atom @@ -59,7 +62,7 @@ public class Feed extends FeedFile { */ public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink, String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, - String downloadUrl, boolean downloaded) { + String downloadUrl, boolean downloaded, FlattrStatus status) { super(fileUrl, downloadUrl, downloaded); this.id = id; this.title = title; @@ -76,10 +79,21 @@ public class Feed extends FeedFile { this.type = type; this.feedIdentifier = feedIdentifier; this.image = image; + this.flattrStatus = status; items = new ArrayList(); } + /** + * This constructor is used for test purposes and uses a default flattr status object. + */ + public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink, + String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, + String downloadUrl, boolean downloaded) { + this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image, + fileUrl, downloadUrl, downloaded, new FlattrStatus()); + } + /** * This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized. */ @@ -87,6 +101,7 @@ public class Feed extends FeedFile { super(); items = new ArrayList(); lastUpdate = new Date(); + this.flattrStatus = new FlattrStatus(); } /** @@ -96,6 +111,7 @@ public class Feed extends FeedFile { public Feed(String url, Date lastUpdate) { super(null, url, false); this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + this.flattrStatus = new FlattrStatus(); } /** @@ -105,6 +121,7 @@ public class Feed extends FeedFile { public Feed(String url, Date lastUpdate, String title) { this(url, lastUpdate); this.title = title; + this.flattrStatus = new FlattrStatus(); } /** @@ -238,6 +255,9 @@ public class Feed extends FeedFile { if (other.paymentLink != null) { paymentLink = other.paymentLink; } + if (other.flattrStatus != null) { + flattrStatus = other.flattrStatus; + } } public boolean compareWithOther(Feed other) { @@ -342,6 +362,14 @@ public class Feed extends FeedFile { this.feedIdentifier = feedIdentifier; } + public void setFlattrStatus(FlattrStatus status) { + this.flattrStatus = status; + } + + public FlattrStatus getFlattrStatus() { + return flattrStatus; + } + public String getPaymentLink() { return paymentLink; } diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java index a80460ece..f63b5beb4 100644 --- a/src/de/danoeh/antennapod/feed/FeedItem.java +++ b/src/de/danoeh/antennapod/feed/FeedItem.java @@ -10,6 +10,8 @@ import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.util.ShownotesProvider; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; /** * Data Object for a XML message @@ -17,7 +19,7 @@ import de.danoeh.antennapod.util.ShownotesProvider; * @author daniel */ public class FeedItem extends FeedComponent implements - ImageLoader.ImageWorkerTaskResource, ShownotesProvider { + ImageLoader.ImageWorkerTaskResource, ShownotesProvider, FlattrThing { /** * The id/guid that can be found in the rss/atom feed. Might not be set. @@ -42,10 +44,12 @@ public class FeedItem extends FeedComponent implements private boolean read; private String paymentLink; + private FlattrStatus flattrStatus; private List chapters; public FeedItem() { this.read = true; + this.flattrStatus = new FlattrStatus(); } /** @@ -59,6 +63,7 @@ public class FeedItem extends FeedComponent implements this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null; this.read = read; this.feed = feed; + this.flattrStatus = new FlattrStatus(); } public void updateFromOther(FeedItem other) { @@ -80,7 +85,7 @@ public class FeedItem extends FeedComponent implements } if (other.media != null) { if (media == null) { - media = other.media; + setMedia(other.media); } else if (media.compareWithOther(other)) { media.updateFromOther(other); } @@ -102,9 +107,9 @@ public class FeedItem extends FeedComponent implements * of the entry. */ public String getIdentifyingValue() { - if (itemIdentifier != null) { + if (itemIdentifier != null && !itemIdentifier.isEmpty()) { return itemIdentifier; - } else if (title != null) { + } else if (title != null && !title.isEmpty()) { return title; } else { return link; @@ -195,7 +200,15 @@ public class FeedItem extends FeedComponent implements this.contentEncoded = contentEncoded; } - public String getPaymentLink() { + public void setFlattrStatus(FlattrStatus status) { + this.flattrStatus = status; + } + + public FlattrStatus getFlattrStatus() { + return flattrStatus; + } + + public String getPaymentLink() { return paymentLink; } diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java index 492867983..fe2c3d17e 100644 --- a/src/de/danoeh/antennapod/feed/FeedMedia.java +++ b/src/de/danoeh/antennapod/feed/FeedMedia.java @@ -1,15 +1,11 @@ package de.danoeh.antennapod.feed; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; - import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.DBReader; @@ -17,7 +13,14 @@ import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.ChapterUtils; import de.danoeh.antennapod.util.playback.Playable; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + public class FeedMedia extends FeedFile implements Playable { + private static final String TAG = "FeedMedia"; public static final int FEEDFILETYPE_FEEDMEDIA = 2; public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; @@ -27,6 +30,7 @@ public class FeedMedia extends FeedFile implements Playable { private int duration; private int position; // Current position in file + private int played_duration; // How many ms of this file have been played (for autoflattring) private long size; // File size in Byte private String mime_type; private volatile FeedItem item; @@ -45,12 +49,13 @@ public class FeedMedia extends FeedFile implements Playable { public FeedMedia(long id, FeedItem item, int duration, int position, long size, String mime_type, String file_url, String download_url, - boolean downloaded, Date playbackCompletionDate) { + boolean downloaded, Date playbackCompletionDate, int played_duration) { super(file_url, download_url, downloaded); this.id = id; this.item = item; this.duration = duration; this.position = position; + this.played_duration = played_duration; this.size = size; this.mime_type = mime_type; this.playbackCompletionDate = playbackCompletionDate == null @@ -137,6 +142,14 @@ public class FeedMedia extends FeedFile implements Playable { this.duration = duration; } + public int getPlayedDuration() { + return played_duration; + } + + public void setPlayedDuration(int played_duration) { + this.played_duration = played_duration; + } + public int getPosition() { return position; } @@ -169,7 +182,7 @@ public class FeedMedia extends FeedFile implements Playable { * Sets the item object of this FeedMedia. If the given * FeedItem object is not null, it's 'media'-attribute value * will also be set to this media object. - * */ + */ public void setItem(FeedItem item) { this.item = item; if (item != null && item.getMedia() != this) { @@ -179,7 +192,8 @@ public class FeedMedia extends FeedFile implements Playable { public Date getPlaybackCompletionDate() { return playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); } + ? null : (Date) playbackCompletionDate.clone(); + } public void setPlaybackCompletionDate(Date playbackCompletionDate) { this.playbackCompletionDate = playbackCompletionDate == null @@ -215,6 +229,7 @@ public class FeedMedia extends FeedFile implements Playable { dest.writeString(download_url); dest.writeByte((byte) ((downloaded) ? 1 : 0)); dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); + dest.writeInt(played_duration); } @Override @@ -313,7 +328,7 @@ public class FeedMedia extends FeedFile implements Playable { @Override public void saveCurrentPosition(SharedPreferences pref, int newPosition) { - position = newPosition; + setPosition(newPosition); DBWriter.setFeedMediaPlaybackInformation(PodcastApp.getInstance(), this); } @@ -358,7 +373,7 @@ public class FeedMedia extends FeedFile implements Playable { final long id = in.readLong(); final long itemID = in.readLong(); FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(), - in.readString(), in.readByte() != 0, new Date(in.readLong())); + in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt()); result.itemID = itemID; return result; } diff --git a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index 3f967bbbe..56e6ee4b8 100644 --- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -14,7 +14,7 @@ import android.widget.TextView; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.playback.Playable; import de.danoeh.antennapod.util.playback.PlaybackController; @@ -137,10 +137,12 @@ public class ExternalPlayerFragment extends Fragment { } @Override - public void loadMediaInfo() { + public boolean loadMediaInfo() { ExternalPlayerFragment fragment = ExternalPlayerFragment.this; if (fragment != null) { - fragment.loadMediaInfo(); + return fragment.loadMediaInfo(); + } else { + return false; } } @@ -209,7 +211,7 @@ public class ExternalPlayerFragment extends Fragment { } } - private void loadMediaInfo() { + private boolean loadMediaInfo() { if (AppConfig.DEBUG) Log.d(TAG, "Loading media info"); if (controller.serviceAvailable()) { @@ -230,13 +232,16 @@ public class ExternalPlayerFragment extends Fragment { } else { butPlay.setVisibility(View.VISIBLE); } + return true; } else { Log.w(TAG, "loadMediaInfo was called while the media object of playbackService was null!"); + return false; } } else { Log.w(TAG, "loadMediaInfo was called while playbackService was null!"); + return false; } } diff --git a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java index ed607b279..d0e07b194 100644 --- a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java @@ -244,19 +244,11 @@ public class FeedlistFragment extends Fragment implements return true; } - private boolean actionModeDestroyWorkaround = false; // TODO remove this workaround - private boolean skipWorkAround = Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH; - @Override public void onDestroyActionMode(ActionMode mode) { - if (skipWorkAround || actionModeDestroyWorkaround) { - mActionMode = null; - selectedFeed = null; - fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); - actionModeDestroyWorkaround = false; - } else { - actionModeDestroyWorkaround = true; - } + mActionMode = null; + selectedFeed = null; + fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); } @Override diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java deleted file mode 100644 index 845a23823..000000000 --- a/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.gpoddernet; - -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; - -/** - * HTTP client for the gpodder.net service. - */ -public class GpodnetClient extends DefaultHttpClient { - - private static SchemeRegistry prepareSchemeRegistry() { - SchemeRegistry sr = new SchemeRegistry(); - - Scheme http = new Scheme("http", - PlainSocketFactory.getSocketFactory(), 80); - sr.register(http); - Scheme https = new Scheme("https", - SSLSocketFactory.getSocketFactory(), 443); - sr.register(https); - - return sr; - } - - @Override - protected ClientConnectionManager createClientConnectionManager() { - return new ThreadSafeClientConnManager(new BasicHttpParams(), prepareSchemeRegistry()); - } - -} diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java index 6e819f570..a0c5b534c 100644 --- a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java @@ -1,9 +1,8 @@ package de.danoeh.antennapod.gpoddernet; -import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.gpoddernet.model.*; import de.danoeh.antennapod.preferences.GpodnetPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.download.AntennapodHttpClient; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -11,15 +10,13 @@ import org.apache.http.HttpStatus; import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; import org.apache.http.impl.auth.BasicScheme; -import org.apache.http.params.CoreProtocolPNames; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -45,16 +42,10 @@ public class GpodnetService { public static final String DEFAULT_BASE_HOST = "gpodder.net"; private final String BASE_HOST; - private static final int TIMEOUT_MILLIS = 20000; - - private final GpodnetClient httpClient; + private final HttpClient httpClient; public GpodnetService() { - httpClient = new GpodnetClient(); - final HttpParams params = httpClient.getParams(); - params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT); - HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_MILLIS); - HttpConnectionParams.setSoTimeout(params, TIMEOUT_MILLIS); + httpClient = AntennapodHttpClient.getHttpClient(); BASE_HOST = GpodnetPreferences.getHostname(); } @@ -519,7 +510,7 @@ public class GpodnetService { new Thread() { @Override public void run() { - httpClient.getConnectionManager().shutdown(); + AntennapodHttpClient.cleanup(); } }.start(); } diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java index f00d6245c..2b4b66362 100644 --- a/src/de/danoeh/antennapod/preferences/UserPreferences.java +++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java @@ -40,6 +40,7 @@ public class UserPreferences implements public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; public static final String PREF_THEME = "prefTheme"; public static final String PREF_DATA_FOLDER = "prefDataFolder"; public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; @@ -50,6 +51,9 @@ public class UserPreferences implements private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + // TODO: Make this value configurable + private static final double PLAYED_DURATION_AUTOFLATTR_THRESHOLD = 0.8; + private static int EPISODE_CACHE_SIZE_UNLIMITED = -1; private static UserPreferences instance; @@ -63,6 +67,7 @@ public class UserPreferences implements private boolean allowMobileUpdate; private boolean displayOnlyEpisodes; private boolean autoDelete; + private boolean autoFlattr; private int theme; private boolean enableAutodownload; private boolean enableAutodownloadWifiFilter; @@ -112,6 +117,7 @@ public class UserPreferences implements allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); theme = readThemeValue(sp.getString(PREF_THEME, "0")); enableAutodownloadWifiFilter = sp.getBoolean( PREF_ENABLE_AUTODL_WIFI_FILTER, false); @@ -223,6 +229,11 @@ public class UserPreferences implements instanceAvailable(); return instance.autoDelete; } + + public static boolean isAutoFlattr() { + instanceAvailable(); + return instance.autoFlattr; + } public static int getTheme() { instanceAvailable(); @@ -296,6 +307,8 @@ public class UserPreferences implements } else if (key.equals(PREF_AUTO_DELETE)) { autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + } else if (key.equals(PREF_AUTO_FLATTR)) { + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); @@ -319,7 +332,9 @@ public class UserPreferences implements PREF_PLAYBACK_SPEED_ARRAY, null)); } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) { pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - } + } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) { + pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + } } public static void setPlaybackSpeed(String speed) { @@ -506,4 +521,9 @@ public class UserPreferences implements instanceAvailable(); return instance.readEpisodeCacheSizeInternal(valueFromPrefs); } + + public static double getPlayedDurationAutoflattrThreshold() { + instanceAvailable(); + return PLAYED_DURATION_AUTOFLATTR_THRESHOLD; + } } diff --git a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java index c57070091..a53ad486a 100644 --- a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java +++ b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.util.Log; import android.view.KeyEvent; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; /** Receives media button events. */ public class MediaButtonReceiver extends BroadcastReceiver { diff --git a/src/de/danoeh/antennapod/receiver/PlayerWidget.java b/src/de/danoeh/antennapod/receiver/PlayerWidget.java index a3d849972..25bb53475 100644 --- a/src/de/danoeh/antennapod/receiver/PlayerWidget.java +++ b/src/de/danoeh/antennapod/receiver/PlayerWidget.java @@ -6,7 +6,7 @@ import android.content.Context; import android.content.Intent; import android.util.Log; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.service.PlayerWidgetService; +import de.danoeh.antennapod.service.playback.PlayerWidgetService; public class PlayerWidget extends AppWidgetProvider { private static final String TAG = "PlayerWidget"; diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java deleted file mode 100644 index 0bb8753c1..000000000 --- a/src/de/danoeh/antennapod/service/PlaybackService.java +++ /dev/null @@ -1,1734 +0,0 @@ -package de.danoeh.antennapod.service; - -import java.io.IOException; -import java.util.Date; -import java.util.List; -import java.util.concurrent.*; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; -import android.media.AudioManager.OnAudioFocusChangeListener; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.media.RemoteControlClient.MetadataEditor; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.view.KeyEvent; -import android.view.SurfaceHolder; -import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AudioplayerActivity; -import de.danoeh.antennapod.activity.VideoplayerActivity; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.receiver.PlayerWidget; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.BitmapDecoder; -import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.DuckType; -import de.danoeh.antennapod.util.flattr.FlattrUtils; -import de.danoeh.antennapod.util.playback.AudioPlayer; -import de.danoeh.antennapod.util.playback.IPlayer; -import de.danoeh.antennapod.util.playback.Playable; -import de.danoeh.antennapod.util.playback.Playable.PlayableException; -import de.danoeh.antennapod.util.playback.VideoPlayer; -import de.danoeh.antennapod.util.playback.PlaybackController; - -/** - * Controls the MediaPlayer that plays a FeedMedia-file - */ -public class PlaybackService extends Service { - /** - * Logging tag - */ - private static final String TAG = "PlaybackService"; - - /** - * Parcelable of type Playable. - */ - public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** - * True if media should be streamed. - */ - public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream"; - /** - * True if playback should be started immediately after media has been - * prepared. - */ - public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared"; - - public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; - - public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; - private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; - - public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification"; - public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode"; - public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType"; - - /** - * If the PlaybackService receives this action, it will stop playback and - * try to shutdown. - */ - public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; - - /** - * If the PlaybackService receives this action, it will end playback of the - * current episode and load the next episode if there is one available. - */ - public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; - - /** - * Used in NOTIFICATION_TYPE_RELOAD. - */ - public static final int EXTRA_CODE_AUDIO = 1; - public static final int EXTRA_CODE_VIDEO = 2; - - public static final int NOTIFICATION_TYPE_ERROR = 0; - public static final int NOTIFICATION_TYPE_INFO = 1; - public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; - - /** - * Receivers of this intent should update their information about the curently playing media - */ - public static final int NOTIFICATION_TYPE_RELOAD = 3; - /** - * The state of the sleeptimer changed. - */ - public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; - public static final int NOTIFICATION_TYPE_BUFFER_START = 5; - public static final int NOTIFICATION_TYPE_BUFFER_END = 6; - /** - * No more episodes are going to be played. - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; - - /** - * Playback speed has changed - * */ - public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; - - /** - * Returned by getPositionSafe() or getDurationSafe() if the playbackService - * is in an invalid state. - */ - public static final int INVALID_TIME = -1; - - /** - * Is true if service is running. - */ - public static boolean isRunning = false; - - private static final int NOTIFICATION_ID = 1; - - private volatile IPlayer player; - private RemoteControlClient remoteControlClient; - private AudioManager audioManager; - private ComponentName mediaButtonReceiver; - - private volatile Playable media; - - /** - * True if media should be streamed (Extracted from Intent Extra) . - */ - private boolean shouldStream; - - private boolean startWhenPrepared; - private PlayerStatus status; - - private PositionSaver positionSaver; - private ScheduledFuture positionSaverFuture; - - private WidgetUpdateWorker widgetUpdater; - private ScheduledFuture widgetUpdaterFuture; - - private SleepTimer sleepTimer; - private Future sleepTimerFuture; - - private static final int SCHED_EX_POOL_SIZE = 3; - private ScheduledThreadPoolExecutor schedExecutor; - private ExecutorService dbLoaderExecutor; - - private volatile PlayerStatus statusBeforeSeek; - - private static boolean playingVideo; - - /** - * True if mediaplayer was paused because it lost audio focus temporarily - */ - private boolean pausedBecauseOfTransientAudiofocusLoss; - - private Thread chapterLoader; - - private final IBinder mBinder = new LocalBinder(); - - private volatile List queue; - - public class LocalBinder extends Binder { - public PlaybackService getService() { - return PlaybackService.this; - } - } - - @Override - public boolean onUnbind(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received onUnbind event"); - return super.onUnbind(intent); - } - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - */ - public static Intent getPlayerActivityIntent(Context context) { - if (isRunning) { - if (playingVideo) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } else { - if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - } - - /** - * Same as getPlayerActivityIntent(context), but here the type of activity - * depends on the FeedMedia that is provided as an argument. - */ - public static Intent getPlayerActivityIntent(Context context, Playable media) { - MediaType mt = media.getMediaType(); - if (mt == MediaType.VIDEO) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - super.onCreate(); - if (AppConfig.DEBUG) - Log.d(TAG, "Service created."); - isRunning = true; - pausedBecauseOfTransientAudiofocusLoss = false; - status = PlayerStatus.STOPPED; - audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, "SchedEx rejected submission of new task"); - } - } - ); - dbLoaderExecutor = Executors.newSingleThreadExecutor(); - - mediaButtonReceiver = new ComponentName(getPackageName(), - MediaButtonReceiver.class.getName()); - audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager - .registerRemoteControlClient(setupRemoteControlClient()); - } - registerReceiver(headsetDisconnected, new IntentFilter( - Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - registerReceiver(audioBecomingNoisy, new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( - ACTION_SKIP_CURRENT_EPISODE)); - EventDistributor.getInstance().register(eventDistributorListener); - loadQueue(); - } - - private IPlayer createMediaPlayer() { - IPlayer player; - if (media == null || media.getMediaType() == MediaType.VIDEO) { - player = new VideoPlayer(); - } else { - player = new AudioPlayer(this); - } - return createMediaPlayer(player); - } - - private IPlayer createMediaPlayer(IPlayer mp) { - if (mp != null && media != null) { - if (media.getMediaType() == MediaType.AUDIO) { - ((AudioPlayer) mp).setOnPreparedListener(audioPreparedListener); - ((AudioPlayer) mp) - .setOnCompletionListener(audioCompletionListener); - ((AudioPlayer) mp) - .setOnSeekCompleteListener(audioSeekCompleteListener); - ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); - ((AudioPlayer) mp) - .setOnBufferingUpdateListener(audioBufferingUpdateListener); - ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); - } else { - ((VideoPlayer) mp).setOnPreparedListener(videoPreparedListener); - ((VideoPlayer) mp) - .setOnCompletionListener(videoCompletionListener); - ((VideoPlayer) mp) - .setOnSeekCompleteListener(videoSeekCompleteListener); - ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); - ((VideoPlayer) mp) - .setOnBufferingUpdateListener(videoBufferingUpdateListener); - ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); - } - } - return mp; - } - - @SuppressLint("NewApi") - @Override - public void onDestroy() { - super.onDestroy(); - if (AppConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); - isRunning = false; - if (chapterLoader != null) { - chapterLoader.interrupt(); - } - disableSleepTimer(); - unregisterReceiver(headsetDisconnected); - unregisterReceiver(shutdownReceiver); - unregisterReceiver(audioBecomingNoisy); - unregisterReceiver(skipCurrentEpisodeReceiver); - EventDistributor.getInstance().unregister(eventDistributorListener); - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager.unregisterRemoteControlClient(remoteControlClient); - } - audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); - audioManager.abandonAudioFocus(audioFocusChangeListener); - player.release(); - stopWidgetUpdater(); - updateWidget(); - } - - @Override - public IBinder onBind(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received onBind event"); - return mBinder; - } - - private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { - loadQueue(); - } - } - }; - - private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() { - - @Override - public void onAudioFocusChange(int focusChange) { - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); - pause(true, false); - stopSelf(); - break; - case AudioManager.AUDIOFOCUS_GAIN: - if (AppConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); - if (pausedBecauseOfTransientAudiofocusLoss) { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, 0); - play(); - } - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - if (status == PlayerStatus.PLAYING) { - if (!UserPreferences.shouldPauseForFocusLoss()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, 0); - pausedBecauseOfTransientAudiofocusLoss = true; - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - if (status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - } - }; - - /** - * 1. Check type of intent - * 1.1 Keycode -> handle keycode -> done - * 1.2 Playable -> Step 2 - * 2. Handle playable - * 2.1 Check current status - * 2.1.1 Not playing -> play new playable - * 2.1.2 Playing, new playable is the same -> play if playback is currently paused - * 2.1.3 Playing, new playable different -> Stop playback of old media - * - * @param intent - * @param flags - * @param startId - * @return - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - if (AppConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); - final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - if (keycode == -1 && playable == null) { - Log.e(TAG, "PlaybackService was started with no arguments"); - stopSelf(); - } - - if (keycode != -1) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received media button event"); - handleKeycode(keycode); - } else { - boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - if (media == null) { - media = playable; - shouldStream = playbackType; - startWhenPrepared = intent.getBooleanExtra( - EXTRA_START_WHEN_PREPARED, false); - initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false)); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } - if (media != null) { - if (!playable.getIdentifier().equals(media.getIdentifier())) { - // different media or different playback type - pause(true, false); - player.reset(); - media = playable; - shouldStream = playbackType; - startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); - initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false)); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } else { - // same media and same playback type - if (status == PlayerStatus.PAUSED) { - play(); - } - } - } - } - - return Service.START_NOT_STICKY; - } - - /** Handles media button events */ - private void handleKeycode(int keycode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling keycode: " + keycode); - switch (keycode) { - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (status == PlayerStatus.PLAYING) { - pause(true, true); - } else if (status == PlayerStatus.PAUSED) { - play(); - } else if (status == PlayerStatus.PREPARING) { - setStartWhenPrepared(!startWhenPrepared); - } else if (status == PlayerStatus.INITIALIZED) { - startWhenPrepared = true; - prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - if (status == PlayerStatus.PAUSED) { - play(); - } else if (status == PlayerStatus.INITIALIZED) { - startWhenPrepared = true; - prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - if (status == PlayerStatus.PLAYING) { - pause(true, true); - } - break; - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { - seekDelta(PlaybackController.DEFAULT_SEEK_DELTA); - break; - } - case KeyEvent.KEYCODE_MEDIA_REWIND: { - seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA); - break; - } - } - } - - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - public void setVideoSurface(SurfaceHolder sh) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting display"); - player.setDisplay(null); - player.setDisplay(sh); - if (status == PlayerStatus.STOPPED - || status == PlayerStatus.AWAITING_VIDEO_SURFACE) { - try { - InitTask initTask = new InitTask() { - - @Override - protected void onPostExecute(Playable result) { - if (status == PlayerStatus.INITIALIZING) { - if (result != null) { - try { - if (shouldStream) { - player.setDataSource(media - .getStreamUrl()); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } else { - player.setDataSource(media - .getLocalMediaUrl()); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } else { - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } - } - - @Override - protected void onPreExecute() { - setStatus(PlayerStatus.INITIALIZING); - } - - }; - initTask.executeAsync(media); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - - } - - /** - * Called when the surface holder of the mediaplayer has to be changed. - */ - private void resetVideoSurface() { - if (AppConfig.DEBUG) - Log.d(TAG, "Resetting video surface"); - cancelPositionSaver(); - player.setDisplay(null); - player.reset(); - player.release(); - player = createMediaPlayer(); - status = PlayerStatus.STOPPED; - } - - public void notifyVideoSurfaceAbandoned() { - resetVideoSurface(); - if (media != null) { - initMediaplayer(true); - } - } - - /** - * Called after service has extracted the media it is supposed to play. - * - * @param prepareImmediately True if service should prepare playback after it has been initialized - */ - private void initMediaplayer(final boolean prepareImmediately) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting up media player"); - try { - MediaType mediaType = media.getMediaType(); - player = createMediaPlayer(); - if (mediaType == MediaType.AUDIO) { - if (AppConfig.DEBUG) - Log.d(TAG, "Mime type is audio"); - - InitTask initTask = new InitTask() { - - @Override - protected void onPostExecute(Playable result) { - // check if state of service has changed. If it has - // changed, assume that loaded metadata is not needed - // anymore. - if (status == PlayerStatus.INITIALIZING) { - if (result != null) { - playingVideo = false; - try { - if (shouldStream) { - player.setDataSource(media - .getStreamUrl()); - } else if (media.localFileAvailable()) { - player.setDataSource(media - .getLocalMediaUrl()); - } - - if (prepareImmediately) { - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } else { - setStatus(PlayerStatus.INITIALIZED); - } - } catch (IOException e) { - e.printStackTrace(); - media = null; - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } else { - Log.e(TAG, "InitTask could not load metadata"); - media = null; - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "Status of player has changed during initialization. Stopping init process."); - } - } - - @Override - protected void onPreExecute() { - setStatus(PlayerStatus.INITIALIZING); - } - - }; - initTask.executeAsync(media); - } else if (mediaType == MediaType.VIDEO) { - if (AppConfig.DEBUG) - Log.d(TAG, "Mime type is video"); - playingVideo = true; - setStatus(PlayerStatus.AWAITING_VIDEO_SURFACE); - player.setScreenOnWhilePlaying(true); - } - - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - - private void setupPositionSaver() { - if (positionSaverFuture == null - || (positionSaverFuture.isCancelled() || positionSaverFuture - .isDone())) { - - positionSaver = new PositionSaver(); - positionSaverFuture = schedExecutor.scheduleAtFixedRate( - positionSaver, PositionSaver.WAITING_INTERVALL, - PositionSaver.WAITING_INTERVALL, TimeUnit.MILLISECONDS); - } - } - - private void cancelPositionSaver() { - if (positionSaverFuture != null) { - boolean result = positionSaverFuture.cancel(true); - if (AppConfig.DEBUG) - Log.d(TAG, "PositionSaver cancelled. Result: " + result); - } - } - - private final com.aocate.media.MediaPlayer.OnPreparedListener audioPreparedListener = new com.aocate.media.MediaPlayer.OnPreparedListener() { - @Override - public void onPrepared(com.aocate.media.MediaPlayer mp) { - genericOnPrepared(mp); - } - }; - - private final android.media.MediaPlayer.OnPreparedListener videoPreparedListener = new android.media.MediaPlayer.OnPreparedListener() { - @Override - public void onPrepared(android.media.MediaPlayer mp) { - genericOnPrepared(mp); - } - }; - - private final void genericOnPrepared(Object inObj) { - IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class); - if (AppConfig.DEBUG) - Log.d(TAG, "Resource prepared"); - mp.seekTo(media.getPosition()); - if (media.getDuration() == 0) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting duration of media"); - media.setDuration(mp.getDuration()); - } - setStatus(PlayerStatus.PREPARED); - if (chapterLoader != null) { - chapterLoader.interrupt(); - } - chapterLoader = new Thread() { - @Override - public void run() { - if (AppConfig.DEBUG) - Log.d(TAG, "Chapter loader started"); - if (media != null && media.getChapters() == null) { - media.loadChapterMarks(); - if (!isInterrupted() && media.getChapters() != null) { - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - 0); - } - } - if (AppConfig.DEBUG) - Log.d(TAG, "Chapter loader stopped"); - } - }; - chapterLoader.start(); - - if (startWhenPrepared) { - play(); - } - } - - private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(com.aocate.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(android.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final void genericSeekCompleteListener() { - if (status == PlayerStatus.SEEKING) { - setStatus(statusBeforeSeek); - } - } - - private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericInfoListener(what); - } - }; - - private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { - return genericInfoListener(what); - } - }; - - private boolean genericInfoListener(int what) { - switch (what) { - case MediaPlayer.MEDIA_INFO_BUFFERING_START: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); - return true; - case MediaPlayer.MEDIA_INFO_BUFFERING_END: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); - return true; - default: - return false; - } - } - - private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericOnError(mp, what, extra); - } - }; - - private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(android.media.MediaPlayer mp, int what, int extra) { - return genericOnError(mp, what, extra); - } - }; - - private boolean genericOnError(Object inObj, int what, int extra) { - final String TAG = "PlaybackService.onErrorListener"; - Log.w(TAG, "An error has occured: " + what + " " + extra); - IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class); - if (mp.isPlaying()) { - pause(true, true); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); - setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - stopSelf(); - return true; - } - - private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(com.aocate.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(android.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private void genericOnCompletion() { - endPlayback(true); - } - - private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, - int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private void genericOnBufferingUpdate(int percent) { - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); - } - - private void endPlayback(boolean playNextEpisode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Playback ended"); - audioManager.abandonAudioFocus(audioFocusChangeListener); - - // Save state - cancelPositionSaver(); - - boolean isInQueue = false; - FeedItem nextItem = null; - - if (media instanceof FeedMedia) { - FeedItem item = ((FeedMedia) media).getItem(); - DBWriter.markItemRead(PlaybackService.this, item, true, true); - nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); - isInQueue = media instanceof FeedMedia - && QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); - } - DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); - long autoDeleteMediaId = ((FeedComponent) media).getId(); - if (shouldStream) { - autoDeleteMediaId = -1; - } - } - - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - boolean loadNextItem = isInQueue && nextItem != null; - playNextEpisode = playNextEpisode && loadNextItem - && UserPreferences.isFollowQueue(); - if (loadNextItem) { - if (AppConfig.DEBUG) - Log.d(TAG, "Loading next item in queue"); - media = nextItem.getMedia(); - } - final boolean prepareImmediately; - if (playNextEpisode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "No more episodes available to play"); - media = null; - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - int notificationCode = 0; - if (media != null) { - shouldStream = !media.localFileAvailable(); - if (media.getMediaType() == MediaType.AUDIO) { - notificationCode = EXTRA_CODE_AUDIO; - playingVideo = false; - } else if (media.getMediaType() == MediaType.VIDEO) { - notificationCode = EXTRA_CODE_VIDEO; - } - } - writePlaybackPreferences(); - if (media != null) { - resetVideoSurface(); - refreshRemoteControlClientState(); - initMediaplayer(prepareImmediately); - - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - notificationCode); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - stopSelf(); - } - } - - public void setSleepTimer(long waitingTime) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); - if (sleepTimerFuture != null) { - sleepTimerFuture.cancel(true); - } - sleepTimer = new SleepTimer(waitingTime); - sleepTimerFuture = schedExecutor.submit(sleepTimer); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - public void disableSleepTimer() { - if (sleepTimerFuture != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Disabling sleep timer"); - sleepTimerFuture.cancel(true); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - } - - /** - * Saves the current position and pauses playback. Note that, if audiofocus - * is abandoned, the lockscreen controls will also disapear. - * - * @param abandonFocus - * is true if the service should release audio focus - * @param reinit - * is true if service should reinit after pausing if the media - * file is being streamed - */ - public void pause(boolean abandonFocus, boolean reinit) { - if (player.isPlaying()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Pausing playback."); - player.pause(); - cancelPositionSaver(); - saveCurrentPosition(); - setStatus(PlayerStatus.PAUSED); - if (abandonFocus) { - audioManager.abandonAudioFocus(audioFocusChangeListener); - pausedBecauseOfTransientAudiofocusLoss = false; - disableSleepTimer(); - } - stopWidgetUpdater(); - stopForeground(true); - if (shouldStream && reinit) { - reinit(); - } - } - } - - /** Pauses playback and destroys service. Recommended for video playback. */ - public void stop() { - if (AppConfig.DEBUG) - Log.d(TAG, "Stopping playback"); - if (status == PlayerStatus.PREPARED || status == PlayerStatus.PAUSED - || status == PlayerStatus.STOPPED - || status == PlayerStatus.PLAYING) { - player.stop(); - } - setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - stopSelf(); - } - - /** - * Prepared media player for playback if the service is in the INITALIZED - * state. - */ - public void prepare() { - if (status == PlayerStatus.INITIALIZED) { - if (AppConfig.DEBUG) - Log.d(TAG, "Preparing media player"); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } - } - - /** Resets the media player and moves into INITIALIZED state. */ - public void reinit() { - player.reset(); - player = createMediaPlayer(player); - initMediaplayer(false); - } - - @SuppressLint("NewApi") - public void play() { - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED - || status == PlayerStatus.STOPPED) { - int focusGained = audioManager.requestAudioFocus( - audioFocusChangeListener, AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - - if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - if (AppConfig.DEBUG) - Log.d(TAG, "Audiofocus successfully requested"); - if (AppConfig.DEBUG) - Log.d(TAG, "Resuming/Starting playback"); - writePlaybackPreferences(); - - setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); - player.start(); - if (status != PlayerStatus.PAUSED) { - player.seekTo((int) media.getPosition()); - } - setStatus(PlayerStatus.PLAYING); - setupPositionSaver(); - setupWidgetUpdater(); - setupNotification(); - pausedBecauseOfTransientAudiofocusLoss = false; - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager - .registerRemoteControlClient(remoteControlClient); - } - audioManager - .registerMediaButtonEventReceiver(mediaButtonReceiver); - media.onPlaybackStart(); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Failed to request Audiofocus"); - } - } - } - - private void writePlaybackPreferences() { - if (AppConfig.DEBUG) - Log.d(TAG, "Writing playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - if (media != null) { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - media.getPlayableType()); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - shouldStream); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, - playingVideo); - if (media instanceof FeedMedia) { - FeedMedia fMedia = (FeedMedia) media; - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - fMedia.getItem().getFeed().getId()); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - fMedia.getId()); - } else { - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - media.writeToPreferences(editor); - } else { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - - editor.commit(); - } - - private void setStatus(PlayerStatus newStatus) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting status to " + newStatus); - status = newStatus; - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - updateWidget(); - refreshRemoteControlClientState(); - bluetoothNotifyChange(); - } - - /** Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. */ - private void postStatusUpdateIntent() { - setStatus(status); - } - - private void sendNotificationBroadcast(int type, int code) { - Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); - intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); - intent.putExtra(EXTRA_NOTIFICATION_CODE, code); - sendBroadcast(intent); - } - - /** Used by setupNotification to load notification data in another thread. */ - private AsyncTask notificationSetupTask; - - /** Prepares notification and starts the service in the foreground. */ - @SuppressLint("NewApi") - private void setupNotification() { - final PendingIntent pIntent = PendingIntent.getActivity(this, 0, - PlaybackService.getPlayerActivityIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); - - if (notificationSetupTask != null) { - notificationSetupTask.cancel(true); - } - notificationSetupTask = new AsyncTask() { - Bitmap icon = null; - - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) - Log.d(TAG, "Starting background work"); - if (android.os.Build.VERSION.SDK_INT >= 11) { - if (media != null && media != null) { - int iconSize = getResources().getDimensionPixelSize( - android.R.dimen.notification_large_icon_width); - icon = BitmapDecoder - .decodeBitmapFromWorkerTaskResource(iconSize, - media); - } - - } - if (icon == null) { - icon = BitmapFactory.decodeResource(getResources(), - R.drawable.ic_stat_antenna); - } - - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (!isCancelled() && status == PlayerStatus.PLAYING - && media != null) { - String contentText = media.getFeedTitle(); - String contentTitle = media.getEpisodeTitle(); - Notification notification = null; - if (android.os.Build.VERSION.SDK_INT >= 16) { - Intent pauseButtonIntent = new Intent( - PlaybackService.this, PlaybackService.class); - pauseButtonIntent.putExtra( - MediaButtonReceiver.EXTRA_KEYCODE, - KeyEvent.KEYCODE_MEDIA_PAUSE); - PendingIntent pauseButtonPendingIntent = PendingIntent - .getService(PlaybackService.this, 0, - pauseButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - Notification.Builder notificationBuilder = new Notification.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setOngoing(true) - .setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna) - .addAction(android.R.drawable.ic_media_pause, - getString(R.string.pause_label), - pauseButtonPendingIntent); - notification = notificationBuilder.build(); - } else { - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna); - notification = notificationBuilder.getNotification(); - } - startForeground(NOTIFICATION_ID, notification); - if (AppConfig.DEBUG) - Log.d(TAG, "Notification set up"); - } - } - - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - notificationSetupTask - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - notificationSetupTask.execute(); - } - - } - - /** - * Seek a specific position from the current position - * - * @param delta - * offset from current position (positive or negative) - * */ - public void seekDelta(int delta) { - int position = getCurrentPositionSafe(); - if (position != INVALID_TIME) { - seek(player.getCurrentPosition() + delta); - } - } - - public void seek(int i) { - saveCurrentPosition(); - if (status == PlayerStatus.INITIALIZED - || status == PlayerStatus.INITIALIZING - || status == PlayerStatus.PREPARING) { - media.setPosition(i); - setStartWhenPrepared(true); - prepare(); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Seeking position " + i); - if (shouldStream) { - if (status != PlayerStatus.SEEKING) { - statusBeforeSeek = status; - } - setStatus(PlayerStatus.SEEKING); - } - player.seekTo(i); - } - } - - public void seekToChapter(Chapter chapter) { - seek((int) chapter.getStart()); - } - - /** Saves the current position of the media file to the DB */ - private synchronized void saveCurrentPosition() { - int position = getCurrentPositionSafe(); - if (position != INVALID_TIME) { - if (AppConfig.DEBUG) - Log.d(TAG, "Saving current position to " + position); - media.saveCurrentPosition(PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()), - position); - } - } - - private void stopWidgetUpdater() { - if (widgetUpdaterFuture != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Stopping widgetUpdateWorker"); - widgetUpdaterFuture.cancel(true); - } - sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE)); - } - - @SuppressLint("NewApi") - private void setupWidgetUpdater() { - if (widgetUpdaterFuture == null - || (widgetUpdaterFuture.isCancelled() || widgetUpdaterFuture - .isDone())) { - widgetUpdater = new WidgetUpdateWorker(); - widgetUpdaterFuture = schedExecutor.scheduleAtFixedRate( - widgetUpdater, WidgetUpdateWorker.NOTIFICATION_INTERVALL, - WidgetUpdateWorker.NOTIFICATION_INTERVALL, - TimeUnit.MILLISECONDS); - } - } - - private void updateWidget() { - if (AppConfig.DEBUG) - Log.d(TAG, "Sending widget update request"); - PlaybackService.this.sendBroadcast(new Intent( - PlayerWidget.FORCE_WIDGET_UPDATE)); - } - - public boolean sleepTimerActive() { - return sleepTimer != null && sleepTimer.isWaiting(); - } - - public long getSleepTimerTimeLeft() { - if (sleepTimerActive()) { - return sleepTimer.getWaitingTime(); - } else { - return 0; - } - } - - @SuppressLint("NewApi") - private RemoteControlClient setupRemoteControlClient() { - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(mediaButtonReceiver); - PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( - getApplicationContext(), 0, mediaButtonIntent, 0); - remoteControlClient = new RemoteControlClient(mediaPendingIntent); - int controlFlags; - if (android.os.Build.VERSION.SDK_INT < 16) { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE - | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; - } else { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; - } - remoteControlClient.setTransportControlFlags(controlFlags); - return remoteControlClient; - } - - /** Refresh player status and metadata. */ - @SuppressLint("NewApi") - private void refreshRemoteControlClientState() { - if (android.os.Build.VERSION.SDK_INT >= 14) { - if (remoteControlClient != null) { - switch (status) { - case PLAYING: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - break; - case PAUSED: - case INITIALIZED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - break; - case STOPPED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - break; - case ERROR: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); - break; - default: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); - } - if (media != null) { - MetadataEditor editor = remoteControlClient - .editMetadata(false); - editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, - media.getEpisodeTitle()); - - editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, - media.getFeedTitle()); - - editor.apply(); - } - if (AppConfig.DEBUG) - Log.d(TAG, "RemoteControlClient state was refreshed"); - } - } - } - - private void bluetoothNotifyChange() { - boolean isPlaying = false; - - if (status == PlayerStatus.PLAYING) { - isPlaying = true; - } - - if (media != null) { - Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED); - i.putExtra("id", 1); - i.putExtra("artist", ""); - i.putExtra("album", media.getFeedTitle()); - i.putExtra("track", media.getEpisodeTitle()); - i.putExtra("playing", isPlaying); - if (queue != null) { - i.putExtra("ListSize", queue.size()); - } - i.putExtra("duration", media.getDuration()); - i.putExtra("position", media.getPosition()); - sendBroadcast(i); - } - } - - /** - * Pauses playback when the headset is disconnected and the preference is - * set - */ - private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { - private static final String TAG = "headsetDisconnected"; - private static final int UNPLUGGED = 0; - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) { - int state = intent.getIntExtra("state", -1); - if (state != -1) { - if (AppConfig.DEBUG) - Log.d(TAG, "Headset plug event. State is " + state); - if (state == UNPLUGGED && status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - Log.d(TAG, "Headset was unplugged during playback."); - pauseIfPauseOnDisconnect(); - } - } else { - Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); - } - } - } - }; - - private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - // sound is about to change, eg. bluetooth -> speaker - if (AppConfig.DEBUG) - Log.d(TAG, "Pausing playback because audio is becoming noisy"); - pauseIfPauseOnDisconnect(); - } - // android.media.AUDIO_BECOMING_NOISY - }; - - /** Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. */ - private void pauseIfPauseOnDisconnect() { - if (UserPreferences.isPauseOnHeadsetDisconnect() - && status == PlayerStatus.PLAYING) { - pause(true, true); - } - } - - private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - schedExecutor.shutdownNow(); - stop(); - media = null; - } - } - - }; - - private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) { - - if (AppConfig.DEBUG) - Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - if (media != null) { - setStatus(PlayerStatus.STOPPED); - endPlayback(true); - } - } - } - }; - - /** Periodically saves the position of the media file */ - class PositionSaver implements Runnable { - public static final int WAITING_INTERVALL = 5000; - - @Override - public void run() { - if (player != null && player.isPlaying()) { - try { - saveCurrentPosition(); - } catch (IllegalStateException e) { - Log.w(TAG, - "saveCurrentPosition was called in illegal state"); - } - } - } - } - - /** Notifies the player widget in the specified intervall */ - class WidgetUpdateWorker implements Runnable { - private static final int NOTIFICATION_INTERVALL = 1000; - - @Override - public void run() { - if (PlaybackService.isRunning) { - updateWidget(); - } - } - } - - /** Sleeps for a given time and then pauses playback. */ - class SleepTimer implements Runnable { - private static final String TAG = "SleepTimer"; - private static final long UPDATE_INTERVALL = 1000L; - private volatile long waitingTime; - private boolean isWaiting; - - public SleepTimer(long waitingTime) { - super(); - this.waitingTime = waitingTime; - } - - @Override - public void run() { - isWaiting = true; - if (AppConfig.DEBUG) - Log.d(TAG, "Starting"); - while (waitingTime > 0) { - try { - Thread.sleep(UPDATE_INTERVALL); - waitingTime -= UPDATE_INTERVALL; - - if (waitingTime <= 0) { - if (AppConfig.DEBUG) - Log.d(TAG, "Waiting completed"); - if (status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - Log.d(TAG, "Pausing playback"); - pause(true, true); - } - postExecute(); - } - } catch (InterruptedException e) { - Log.d(TAG, "Thread was interrupted while waiting"); - break; - } - } - postExecute(); - } - - protected void postExecute() { - isWaiting = false; - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - public long getWaitingTime() { - return waitingTime; - } - - public boolean isWaiting() { - return isWaiting; - } - - } - - public static boolean isPlayingVideo() { - return playingVideo; - } - - public boolean isShouldStream() { - return shouldStream; - } - - public PlayerStatus getStatus() { - return status; - } - - public Playable getMedia() { - return media; - } - - public IPlayer getPlayer() { - return player; - } - - public boolean isStartWhenPrepared() { - return startWhenPrepared; - } - - public void setStartWhenPrepared(boolean startWhenPrepared) { - this.startWhenPrepared = startWhenPrepared; - postStatusUpdateIntent(); - } - - public boolean canSetSpeed() { - if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) { - return ((AudioPlayer) player).canSetSpeed(); - } - return false; - } - - public boolean canSetPitch() { - if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) { - return ((AudioPlayer) player).canSetPitch(); - } - return false; - } - - public void setSpeed(float speed) { - if (media != null && media.getMediaType() == MediaType.AUDIO) { - AudioPlayer audioPlayer = (AudioPlayer) player; - if (audioPlayer.canSetSpeed()) { - audioPlayer.setPlaybackSpeed((float) speed); - if (AppConfig.DEBUG) - Log.d(TAG, "Playback speed was set to " + speed); - sendNotificationBroadcast( - NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); - } - } - } - - public void setPitch(float pitch) { - if (media != null && media.getMediaType() == MediaType.AUDIO) { - AudioPlayer audioPlayer = (AudioPlayer) player; - if (audioPlayer.canSetPitch()) { - audioPlayer.setPlaybackPitch((float) pitch); - } - } - } - - public float getCurrentPlaybackSpeed() { - if (media.getMediaType() == MediaType.AUDIO - && player instanceof AudioPlayer) { - AudioPlayer audioPlayer = (AudioPlayer) player; - if (audioPlayer.canSetSpeed()) { - return audioPlayer.getCurrentSpeedMultiplier(); - } - } - return -1; - } - - /** - * call getDuration() on mediaplayer or return INVALID_TIME if player is in - * an invalid state. This method should be used instead of calling - * getDuration() directly to avoid an error. - */ - public int getDurationSafe() { - if (status != null && player != null) { - switch (status) { - case PREPARED: - case PLAYING: - case PAUSED: - case SEEKING: - try { - return player.getDuration(); - } catch (IllegalStateException e) { - e.printStackTrace(); - return INVALID_TIME; - } - default: - return INVALID_TIME; - } - } else { - return INVALID_TIME; - } - } - - /** - * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player - * is in an invalid state. This method should be used instead of calling - * getCurrentPosition() directly to avoid an error. - */ - public int getCurrentPositionSafe() { - if (status != null && player != null) { - switch (status) { - case PREPARED: - case PLAYING: - case PAUSED: - case SEEKING: - return player.getCurrentPosition(); - default: - return INVALID_TIME; - } - } else { - return INVALID_TIME; - } - } - - private void setCurrentlyPlayingMedia(long id) { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id); - editor.commit(); - } - - private static class InitTask extends AsyncTask { - private Playable playable; - public PlayableException exception; - - @Override - protected Playable doInBackground(Playable... params) { - if (params[0] == null) { - throw new IllegalArgumentException("Playable must not be null"); - } - playable = params[0]; - - try { - playable.loadMetadata(); - } catch (PlayableException e) { - e.printStackTrace(); - exception = e; - return null; - } - return playable; - } - - @SuppressLint("NewApi") - public void executeAsync(Playable playable) { - FlattrUtils.hasToken(); - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(THREAD_POOL_EXECUTOR, playable); - } else { - execute(playable); - } - } - - } - - private void loadQueue() { - dbLoaderExecutor.submit(new QueueLoaderTask()); - } - - private class QueueLoaderTask implements Runnable { - @Override - public void run() { - List queueRef = DBReader.getQueue(PlaybackService.this); - queue = queueRef; - } - } -} diff --git a/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java new file mode 100644 index 000000000..7e1c9178a --- /dev/null +++ b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java @@ -0,0 +1,95 @@ +package de.danoeh.antennapod.service.download; + +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import org.apache.http.client.HttpClient; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.params.ConnManagerPNames; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.CoreProtocolPNames; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import java.util.concurrent.TimeUnit; + +/** + * Provides access to a HttpClient singleton. + */ +public class AntennapodHttpClient { + private static final String TAG = "AntennapodHttpClient"; + + public static final long EXPIRED_CONN_TIMEOUT_SEC = 30; + + public static final int MAX_REDIRECTS = 5; + public static final int CONNECTION_TIMEOUT = 30000; + public static final int SOCKET_TIMEOUT = 30000; + + public static final int MAX_CONNECTIONS = 6; + + + private static volatile HttpClient httpClient = null; + + /** + * Returns the HttpClient singleton. + */ + public static synchronized HttpClient getHttpClient() { + if (httpClient == null) { + if (AppConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client"); + + HttpParams params = new BasicHttpParams(); + params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT); + params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS); + params.setBooleanParameter("http.protocol.reject-relative-redirect", + false); + HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); + HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); + HttpClientParams.setRedirecting(params, true); + + httpClient = new DefaultHttpClient(createClientConnectionManager(), params); + // Workaround for broken URLs in redirection + ((AbstractHttpClient) httpClient) + .setRedirectHandler(new APRedirectHandler()); + } + return httpClient; + } + + /** + * Closes expired connections. This method should be called by the using class once has finished its work with + * the HTTP client. + */ + public static synchronized void cleanup() { + if (httpClient != null) { + httpClient.getConnectionManager().closeExpiredConnections(); + httpClient.getConnectionManager().closeIdleConnections(EXPIRED_CONN_TIMEOUT_SEC, TimeUnit.SECONDS); + } + } + + + private static ClientConnectionManager createClientConnectionManager() { + HttpParams params = new BasicHttpParams(); + params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, MAX_CONNECTIONS); + return new ThreadSafeClientConnManager(params, prepareSchemeRegistry()); + } + + private static SchemeRegistry prepareSchemeRegistry() { + SchemeRegistry sr = new SchemeRegistry(); + + Scheme http = new Scheme("http", + PlainSocketFactory.getSocketFactory(), 80); + sr.register(http); + Scheme https = new Scheme("https", + SSLSocketFactory.getSocketFactory(), 443); + sr.register(https); + + return sr; + } + +} diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index 4d521b4df..c27b4d4fe 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -11,6 +11,7 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.ParserConfigurationException; +import android.media.MediaMetadataRetriever; import de.danoeh.antennapod.storage.*; import org.xml.sax.SAXException; @@ -25,7 +26,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.media.MediaPlayer; import android.os.AsyncTask; import android.os.Binder; import android.os.Handler; @@ -801,23 +801,21 @@ public class DownloadService extends Service { media.setFile_url(request.getDestination()); // Get duration - MediaPlayer mediaplayer = null; + MediaMetadataRetriever mmr = null; try { - mediaplayer = new MediaPlayer(); - mediaplayer.setDataSource(media.getFile_url()); - mediaplayer.prepare(); - media.setDuration(mediaplayer.getDuration()); + mmr = new MediaMetadataRetriever(); + mmr.setDataSource(media.getFile_url()); + String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + media.setDuration(Integer.parseInt(durationStr)); if (AppConfig.DEBUG) Log.d(TAG, "Duration of file is " + media.getDuration()); - mediaplayer.reset(); - } catch (IOException e) { + } catch (NumberFormatException e) { e.printStackTrace(); } catch (RuntimeException e) { - // Thrown by MediaPlayer initialization on some devices e.printStackTrace(); } finally { - if (mediaplayer != null) { - mediaplayer.release(); + if (mmr != null) { + mmr.release(); } } diff --git a/src/de/danoeh/antennapod/service/download/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java index 84731fe9f..80cc5b3f8 100644 --- a/src/de/danoeh/antennapod/service/download/Downloader.java +++ b/src/de/danoeh/antennapod/service/download/Downloader.java @@ -1,5 +1,8 @@ package de.danoeh.antennapod.service.download; +import android.content.Context; +import android.net.wifi.WifiManager; +import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import java.util.concurrent.Callable; @@ -26,7 +29,19 @@ public abstract class Downloader implements Callable { protected abstract void download(); public final Downloader call() { + WifiManager wifiManager = (WifiManager) PodcastApp.getInstance().getSystemService(Context.WIFI_SERVICE); + WifiManager.WifiLock wifiLock = null; + if (wifiManager != null) { + wifiLock = wifiManager.createWifiLock(TAG); + wifiLock.acquire(); + } + download(); + + if (wifiLock != null) { + wifiLock.release(); + } + if (result == null) { throw new IllegalStateException( "Downloader hasn't created DownloadStatus object"); diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java index 582fb9575..fc2b3178b 100644 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java @@ -1,26 +1,5 @@ package de.danoeh.antennapod.service.download; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; - -import org.apache.commons.io.IOUtils; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.impl.client.AbstractHttpClient; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; - import android.net.http.AndroidHttpClient; import android.util.Log; import de.danoeh.antennapod.AppConfig; @@ -28,44 +7,43 @@ import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.StorageUtils; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; + +import java.io.*; +import java.net.*; public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; - private static final int MAX_REDIRECTS = 5; - private static final int BUFFER_SIZE = 8 * 1024; - private static final int CONNECTION_TIMEOUT = 30000; - private static final int SOCKET_TIMEOUT = 30000; public HttpDownloader(DownloadRequest request) { super(request); } - private DefaultHttpClient createHttpClient() { - DefaultHttpClient httpClient = new DefaultHttpClient(); - HttpParams params = httpClient.getParams(); - params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS); - params.setBooleanParameter("http.protocol.reject-relative-redirect", - false); - HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); - HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); - HttpClientParams.setRedirecting(params, true); - - // Workaround for broken URLs in redirection - ((AbstractHttpClient) httpClient) - .setRedirectHandler(new APRedirectHandler()); - return httpClient; + private URI getURIFromRequestUrl(String source) { + try { + URL url = new URL(source); + return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } } @Override protected void download() { - DefaultHttpClient httpClient = null; + HttpClient httpClient = AntennapodHttpClient.getHttpClient(); BufferedOutputStream out = null; InputStream connection = null; try { - HttpGet httpGet = new HttpGet(request.getSource()); - httpClient = createHttpClient(); + HttpGet httpGet = new HttpGet(getURIFromRequestUrl(request.getSource())); HttpResponse response = httpClient.execute(httpGet); HttpEntity httpEntity = response.getEntity(); int responseCode = response.getStatusLine().getStatusCode(); @@ -167,9 +145,7 @@ public class HttpDownloader extends Downloader { onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); } finally { IOUtils.closeQuietly(out); - if (httpClient != null) { - httpClient.getConnectionManager().shutdown(); - } + AntennapodHttpClient.cleanup(); } } diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java new file mode 100644 index 000000000..6bc8c4127 --- /dev/null +++ b/src/de/danoeh/antennapod/service/playback/PlaybackService.java @@ -0,0 +1,1037 @@ +package de.danoeh.antennapod.service.playback; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.*; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.media.RemoteControlClient.MetadataEditor; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.AudioplayerActivity; +import de.danoeh.antennapod.activity.VideoplayerActivity; +import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.feed.MediaType; +import de.danoeh.antennapod.preferences.PlaybackPreferences; +import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.receiver.PlayerWidget; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.BitmapDecoder; +import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.flattr.FlattrUtils; +import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.util.playback.PlaybackController; + +import java.util.List; + +/** + * Controls the MediaPlayer that plays a FeedMedia-file + */ +public class PlaybackService extends Service { + /** + * Logging tag + */ + private static final String TAG = "PlaybackService"; + + /** + * Parcelable of type Playable. + */ + public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; + /** + * True if media should be streamed. + */ + public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream"; + /** + * True if playback should be started immediately after media has been + * prepared. + */ + public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared"; + + public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; + + public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; + private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; + private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; + + public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification"; + public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode"; + public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType"; + + /** + * If the PlaybackService receives this action, it will stop playback and + * try to shutdown. + */ + public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; + + /** + * If the PlaybackService receives this action, it will end playback of the + * current episode and load the next episode if there is one available. + */ + public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; + + /** + * Used in NOTIFICATION_TYPE_RELOAD. + */ + public static final int EXTRA_CODE_AUDIO = 1; + public static final int EXTRA_CODE_VIDEO = 2; + + public static final int NOTIFICATION_TYPE_ERROR = 0; + public static final int NOTIFICATION_TYPE_INFO = 1; + public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; + + /** + * Receivers of this intent should update their information about the curently playing media + */ + public static final int NOTIFICATION_TYPE_RELOAD = 3; + /** + * The state of the sleeptimer changed. + */ + public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; + public static final int NOTIFICATION_TYPE_BUFFER_START = 5; + public static final int NOTIFICATION_TYPE_BUFFER_END = 6; + /** + * No more episodes are going to be played. + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; + + /** + * Playback speed has changed + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; + + /** + * Returned by getPositionSafe() or getDurationSafe() if the playbackService + * is in an invalid state. + */ + public static final int INVALID_TIME = -1; + + /** + * Is true if service is running. + */ + public static boolean isRunning = false; + /** + * Is true if service has received a valid start command. + */ + public static boolean started = false; + + private static final int NOTIFICATION_ID = 1; + + private RemoteControlClient remoteControlClient; + private PlaybackServiceMediaPlayer mediaPlayer; + private PlaybackServiceTaskManager taskManager; + + private static volatile MediaType currentMediaType = MediaType.UNKNOWN; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public PlaybackService getService() { + return PlaybackService.this; + } + } + + @Override + public boolean onUnbind(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received onUnbind event"); + return super.onUnbind(intent); + } + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. If the playbackservice is not + * running, the type of the last played media will be looked up. + */ + public static Intent getPlayerActivityIntent(Context context) { + if (isRunning) { + if (currentMediaType == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } else { + if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + } + + /** + * Same as getPlayerActivityIntent(context), but here the type of activity + * depends on the FeedMedia that is provided as an argument. + */ + public static Intent getPlayerActivityIntent(Context context, Playable media) { + MediaType mt = media.getMediaType(); + if (mt == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + super.onCreate(); + if (AppConfig.DEBUG) + Log.d(TAG, "Service created."); + isRunning = true; + + registerReceiver(headsetDisconnected, new IntentFilter( + Intent.ACTION_HEADSET_PLUG)); + registerReceiver(shutdownReceiver, new IntentFilter( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + registerReceiver(audioBecomingNoisy, new IntentFilter( + AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( + ACTION_SKIP_CURRENT_EPISODE)); + remoteControlClient = setupRemoteControlClient(); + taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); + mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); + + } + + @SuppressLint("NewApi") + @Override + public void onDestroy() { + super.onDestroy(); + if (AppConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + isRunning = false; + started = false; + currentMediaType = MediaType.UNKNOWN; + + unregisterReceiver(headsetDisconnected); + unregisterReceiver(shutdownReceiver); + unregisterReceiver(audioBecomingNoisy); + unregisterReceiver(skipCurrentEpisodeReceiver); + mediaPlayer.shutdown(); + taskManager.shutdown(); + } + + @Override + public IBinder onBind(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received onBind event"); + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + if (AppConfig.DEBUG) + Log.d(TAG, "OnStartCommand called"); + final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); + if (keycode == -1 && playable == null) { + Log.e(TAG, "PlaybackService was started with no arguments"); + stopSelf(); + } + + if ((flags & Service.START_FLAG_REDELIVERY) != 0) { + if (AppConfig.DEBUG) Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + stopForeground(true); + } else { + + if (keycode != -1) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received media button event"); + handleKeycode(keycode); + } else { + started = true; + boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, + true); + boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); + boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); + } + } + + return Service.START_REDELIVER_INTENT; + } + + /** + * Handles media button events + */ + private void handleKeycode(int keycode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling keycode: " + keycode); + + final PlayerStatus status = mediaPlayer.getPSMPInfo().playerStatus; + switch (keycode) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if (status == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, true); + } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.PREPARING) { + mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + if (status == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, true); + } + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + mediaPlayer.seekDelta(PlaybackController.DEFAULT_SEEK_DELTA); + break; + } + case KeyEvent.KEYCODE_MEDIA_REWIND: { + mediaPlayer.seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA); + break; + } + } + } + + /** + * Called by a mediaplayer Activity as soon as it has prepared its + * mediaplayer. + */ + public void setVideoSurface(SurfaceHolder sh) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting display"); + mediaPlayer.setVideoSurface(sh); + } + + /** + * Called when the surface holder of the mediaplayer has to be changed. + */ + private void resetVideoSurface() { + taskManager.cancelPositionSaver(); + mediaPlayer.resetVideoSurface(); + } + + public void notifyVideoSurfaceAbandoned() { + stopForeground(true); + mediaPlayer.resetVideoSurface(); + } + + private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); + } + + @Override + public void onSleepTimerExpired() { + mediaPlayer.pause(true, true); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + + @Override + public void onWidgetUpdaterTick() { + updateWidget(); + } + + @Override + public void onChapterLoaded(Playable media) { + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + } + }; + + private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + currentMediaType = mediaPlayer.getCurrentMediaType(); + switch (newInfo.playerStatus) { + case INITIALIZED: + writePlaybackPreferences(); + break; + + case PREPARED: + taskManager.startChapterLoader(newInfo.playable); + break; + + case PAUSED: + taskManager.cancelPositionSaver(); + saveCurrentPosition(false, 0); + taskManager.cancelWidgetUpdater(); + stopForeground(true); + break; + + case STOPPED: + //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); + //stopSelf(); + break; + + case PLAYING: + if (AppConfig.DEBUG) + Log.d(TAG, "Audiofocus successfully requested"); + if (AppConfig.DEBUG) + Log.d(TAG, "Resuming/Starting playback"); + + taskManager.startPositionSaver(); + taskManager.startWidgetUpdater(); + setupNotification(newInfo); + break; + + } + + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + updateWidget(); + refreshRemoteControlClientState(newInfo); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); + } + + @Override + public void shouldStop() { + stopSelf(); + } + + @Override + public void playbackSpeedChanged(float s) { + sendNotificationBroadcast( + NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); + } + + @Override + public void onBufferingUpdate(int percent) { + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); + } + + @Override + public boolean onMediaPlayerInfo(int code) { + switch (code) { + case MediaPlayer.MEDIA_INFO_BUFFERING_START: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); + return true; + case MediaPlayer.MEDIA_INFO_BUFFERING_END: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); + return true; + default: + return false; + } + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + final String TAG = "PlaybackService.onErrorListener"; + Log.w(TAG, "An error has occured: " + what + " " + extra); + if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, false); + } + sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); + setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); + stopSelf(); + return true; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + PlaybackService.this.endPlayback(true); + return true; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return remoteControlClient; + } + }; + + private void endPlayback(boolean playNextEpisode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Playback ended"); + + final Playable media = mediaPlayer.getPSMPInfo().playable; + if (media == null) { + Log.e(TAG, "Cannot end playback: media was null"); + return; + } + + taskManager.cancelPositionSaver(); + + boolean isInQueue = false; + FeedItem nextItem = null; + + if (media instanceof FeedMedia) { + FeedItem item = ((FeedMedia) media).getItem(); + DBWriter.markItemRead(PlaybackService.this, item, true, true); + + try { + final List queue = taskManager.getQueue(); + isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); + nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); + } catch (InterruptedException e) { + e.printStackTrace(); + // isInQueue remains false + } + if (isInQueue) { + DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); + } + DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + } + + // Load next episode if previous episode was in the queue and if there + // is an episode in the queue left. + // Start playback immediately if continuous playback is enabled + Playable nextMedia = null; + boolean loadNextItem = isInQueue && nextItem != null; + playNextEpisode = playNextEpisode && loadNextItem + && UserPreferences.isFollowQueue(); + if (loadNextItem) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading next item in queue"); + nextMedia = nextItem.getMedia(); + } + final boolean prepareImmediately; + final boolean startWhenPrepared; + final boolean stream; + + if (playNextEpisode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Playback of next episode will start immediately."); + prepareImmediately = startWhenPrepared = true; + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "No more episodes available to play"); + + prepareImmediately = startWhenPrepared = false; + stopForeground(true); + stopWidgetUpdater(); + } + + writePlaybackPreferences(); + if (nextMedia != null) { + stream = !media.localFileAvailable(); + mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); + } else { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + //stopSelf(); + } + } + + public void setSleepTimer(long waitingTime) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + taskManager.setSleepTimer(waitingTime); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + public void disableSleepTimer() { + taskManager.disableSleepTimer(); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + + private void writePlaybackPreferences() { + if (AppConfig.DEBUG) + Log.d(TAG, "Writing playback preferences"); + + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + MediaType mediaType = mediaPlayer.getCurrentMediaType(); + boolean stream = mediaPlayer.isStreaming(); + + if (info.playable != null) { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + info.playable.getPlayableType()); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, + stream); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, + mediaType == MediaType.VIDEO); + if (info.playable instanceof FeedMedia) { + FeedMedia fMedia = (FeedMedia) info.playable; + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + fMedia.getItem().getFeed().getId()); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + fMedia.getId()); + } else { + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + info.playable.writeToPreferences(editor); + } else { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + + editor.commit(); + } + + /** + * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. + */ + private void postStatusUpdateIntent() { + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + } + + private void sendNotificationBroadcast(int type, int code) { + Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); + intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); + intent.putExtra(EXTRA_NOTIFICATION_CODE, code); + sendBroadcast(intent); + } + + /** + * Used by setupNotification to load notification data in another thread. + */ + private AsyncTask notificationSetupTask; + + /** + * Prepares notification and starts the service in the foreground. + */ + @SuppressLint("NewApi") + private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { + final PendingIntent pIntent = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT); + + if (notificationSetupTask != null) { + notificationSetupTask.cancel(true); + } + notificationSetupTask = new AsyncTask() { + Bitmap icon = null; + + @Override + protected Void doInBackground(Void... params) { + if (AppConfig.DEBUG) + Log.d(TAG, "Starting background work"); + if (android.os.Build.VERSION.SDK_INT >= 11) { + if (info.playable != null) { + int iconSize = getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + icon = BitmapDecoder + .decodeBitmapFromWorkerTaskResource(iconSize, + info.playable); + } + + } + if (icon == null) { + icon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_stat_antenna); + } + + return null; + } + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING + && info.playable != null) { + String contentText = info.playable.getFeedTitle(); + String contentTitle = info.playable.getEpisodeTitle(); + Notification notification = null; + if (android.os.Build.VERSION.SDK_INT >= 16) { + Intent pauseButtonIntent = new Intent( + PlaybackService.this, PlaybackService.class); + pauseButtonIntent.putExtra( + MediaButtonReceiver.EXTRA_KEYCODE, + KeyEvent.KEYCODE_MEDIA_PAUSE); + PendingIntent pauseButtonPendingIntent = PendingIntent + .getService(PlaybackService.this, 0, + pauseButtonIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + Notification.Builder notificationBuilder = new Notification.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setOngoing(true) + .setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_stat_antenna) + .addAction(android.R.drawable.ic_media_pause, + getString(R.string.pause_label), + pauseButtonPendingIntent); + notification = notificationBuilder.build(); + } else { + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText).setOngoing(true) + .setContentIntent(pIntent).setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_stat_antenna); + notification = notificationBuilder.getNotification(); + } + startForeground(NOTIFICATION_ID, notification); + if (AppConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + } + + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + notificationSetupTask + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + notificationSetupTask.execute(); + } + + } + + /** + * Saves the current position of the media file to the DB + * + * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects + * @param deltaPlayedDuration value by which played_duration should be increased. + */ + private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { + int position = getCurrentPosition(); + int duration = getDuration(); + float playbackSpeed = getCurrentPlaybackSpeed(); + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Saving current position to " + position); + if (updatePlayedDuration && playable instanceof FeedMedia) { + FeedMedia m = (FeedMedia) playable; + FeedItem item = m.getItem(); + m.setPlayedDuration(m.getPlayedDuration() + ((int)(deltaPlayedDuration * playbackSpeed))); + // Auto flattr + if (FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred() && + (m.getPlayedDuration() > UserPreferences.getPlayedDurationAutoflattrThreshold() * duration)) { + + if (AppConfig.DEBUG) + Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) + + " is " + UserPreferences.getPlayedDurationAutoflattrThreshold() * 100 + "% of file duration " + Integer.toString(duration)); + item.getFlattrStatus().setFlattrQueue(); + DBWriter.setFeedItemFlattrStatus(PodcastApp.getInstance(), item, false); + } + } + playable.saveCurrentPosition(PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()), + position); + } + } + + private void stopWidgetUpdater() { + taskManager.cancelWidgetUpdater(); + sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE)); + } + + private void updateWidget() { + PlaybackService.this.sendBroadcast(new Intent( + PlayerWidget.FORCE_WIDGET_UPDATE)); + } + + public boolean sleepTimerActive() { + return taskManager.isSleepTimerActive(); + } + + public long getSleepTimerTimeLeft() { + return taskManager.getSleepTimerTimeLeft(); + } + + @SuppressLint("NewApi") + private RemoteControlClient setupRemoteControlClient() { + if (Build.VERSION.SDK_INT < 14) { + return null; + } + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(new ComponentName(getPackageName(), + MediaButtonReceiver.class.getName())); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( + getApplicationContext(), 0, mediaButtonIntent, 0); + remoteControlClient = new RemoteControlClient(mediaPendingIntent); + int controlFlags; + if (android.os.Build.VERSION.SDK_INT < 16) { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } else { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + remoteControlClient.setTransportControlFlags(controlFlags); + return remoteControlClient; + } + + /** + * Refresh player status and metadata. + */ + @SuppressLint("NewApi") + private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) { + if (android.os.Build.VERSION.SDK_INT >= 14) { + if (remoteControlClient != null) { + switch (info.playerStatus) { + case PLAYING: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + break; + case PAUSED: + case INITIALIZED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + break; + case STOPPED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + break; + case ERROR: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); + break; + default: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); + } + if (info.playable != null) { + MetadataEditor editor = remoteControlClient + .editMetadata(false); + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, + info.playable.getEpisodeTitle()); + + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, + info.playable.getFeedTitle()); + + editor.apply(); + } + if (AppConfig.DEBUG) + Log.d(TAG, "RemoteControlClient state was refreshed"); + } + } + } + + private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { + boolean isPlaying = false; + + if (info.playerStatus == PlayerStatus.PLAYING) { + isPlaying = true; + } + + if (info.playable != null) { + Intent i = new Intent(whatChanged); + i.putExtra("id", 1); + i.putExtra("artist", ""); + i.putExtra("album", info.playable.getFeedTitle()); + i.putExtra("track", info.playable.getEpisodeTitle()); + i.putExtra("playing", isPlaying); + final List queue = taskManager.getQueueIfLoaded(); + if (queue != null) { + i.putExtra("ListSize", queue.size()); + } + i.putExtra("duration", info.playable.getDuration()); + i.putExtra("position", info.playable.getPosition()); + sendBroadcast(i); + } + } + + /** + * Pauses playback when the headset is disconnected and the preference is + * set + */ + private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { + private static final String TAG = "headsetDisconnected"; + private static final int UNPLUGGED = 0; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && + intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) { + int state = intent.getIntExtra("state", -1); + if (state != -1) { + if (AppConfig.DEBUG) + Log.d(TAG, "Headset plug event. State is " + state); + if (state == UNPLUGGED) { + if (AppConfig.DEBUG) + Log.d(TAG, "Headset was unplugged during playback."); + pauseIfPauseOnDisconnect(); + } + } else { + Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); + } + } + } + }; + + private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + // sound is about to change, eg. bluetooth -> speaker + if (AppConfig.DEBUG) + Log.d(TAG, "Pausing playback because audio is becoming noisy"); + pauseIfPauseOnDisconnect(); + } + // android.media.AUDIO_BECOMING_NOISY + }; + + /** + * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. + */ + private void pauseIfPauseOnDisconnect() { + if (UserPreferences.isPauseOnHeadsetDisconnect()) { + mediaPlayer.pause(true, true); + } + } + + private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && + intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + stopSelf(); + } + } + + }; + + private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && + intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); + mediaPlayer.endPlayback(); + } + } + }; + + public static MediaType getCurrentMediaType() { + return currentMediaType; + } + + public void resume() { + mediaPlayer.resume(); + } + + public void prepare() { + mediaPlayer.prepare(); + } + + public void pause(boolean abandonAudioFocus, boolean reinit) { + mediaPlayer.pause(abandonAudioFocus, reinit); + } + + public void reinit() { + mediaPlayer.reinit(); + } + + public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { + return mediaPlayer.getPSMPInfo(); + } + + public PlayerStatus getStatus() { + return mediaPlayer.getPSMPInfo().playerStatus; + } + + public Playable getPlayable() { + return mediaPlayer.getPSMPInfo().playable; + } + + public void setSpeed(float speed) { + mediaPlayer.setSpeed(speed); + } + + public boolean canSetSpeed() { + return mediaPlayer.canSetSpeed(); + } + + public float getCurrentPlaybackSpeed() { + return mediaPlayer.getPlaybackSpeed(); + } + + public boolean isStartWhenPrepared() { + return mediaPlayer.isStartWhenPrepared(); + } + + public void setStartWhenPrepared(boolean s) { + mediaPlayer.setStartWhenPrepared(s); + } + + + public void seekTo(final int t) { + mediaPlayer.seekTo(t); + } + + + public void seekDelta(final int d) { + mediaPlayer.seekDelta(d); + } + + /** + * @see de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.feed.Chapter) + */ + public void seekToChapter(Chapter c) { + mediaPlayer.seekToChapter(c); + } + + /** + * call getDuration() on mediaplayer or return INVALID_TIME if player is in + * an invalid state. + */ + public int getDuration() { + return mediaPlayer.getDuration(); + } + + /** + * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player + * is in an invalid state. + */ + public int getCurrentPosition() { + return mediaPlayer.getPosition(); + } + + public boolean isStreaming() { + return mediaPlayer.isStreaming(); + } + + public Pair getVideoSize() { + return mediaPlayer.getVideoSize(); + } + + private void setCurrentlyPlayingMedia(long id) { + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id); + editor.commit(); + } +} diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java new file mode 100644 index 000000000..30f6de458 --- /dev/null +++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java @@ -0,0 +1,923 @@ +package de.danoeh.antennapod.service.playback; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.util.Log; +import android.util.Pair; +import android.view.SurfaceHolder; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.MediaType; +import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.util.playback.AudioPlayer; +import de.danoeh.antennapod.util.playback.IPlayer; +import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.util.playback.VideoPlayer; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Manages the MediaPlayer object of the PlaybackService. + */ +public class PlaybackServiceMediaPlayer { + public static final String TAG = "PlaybackServiceMediaPlayer"; + + /** + * Return value of some PSMP methods if the method call failed. + */ + public static final int INVALID_TIME = -1; + + private final AudioManager audioManager; + + private volatile PlayerStatus playerStatus; + private volatile PlayerStatus statusBeforeSeeking; + private volatile IPlayer mediaPlayer; + private volatile Playable media; + + private volatile boolean stream; + private volatile MediaType mediaType; + private volatile AtomicBoolean startWhenPrepared; + private volatile boolean pausedBecauseOfTransientAudiofocusLoss; + private volatile Pair videoSize; + + /** + * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads + * have to wait until these operations have finished. + */ + private final ReentrantLock playerLock; + + private final PSMPCallback callback; + private final Context context; + + private final ThreadPoolExecutor executor; + + public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { + if (context == null) + throw new IllegalArgumentException("context = null"); + if (callback == null) + throw new IllegalArgumentException("callback = null"); + + this.context = context; + this.callback = callback; + this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + this.playerLock = new ReentrantLock(); + this.startWhenPrepared = new AtomicBoolean(false); + executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque(), + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + if (AppConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); + } + }); + + mediaPlayer = null; + statusBeforeSeeking = null; + pausedBecauseOfTransientAudiofocusLoss = false; + mediaType = MediaType.UNKNOWN; + playerStatus = PlayerStatus.STOPPED; + videoSize = null; + } + + /** + * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing + * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will + * not do anything. + * Whether playback starts immediately depends on the given parameters. See below for more details. + *

+ * States: + * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. + *

+ * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If + * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. + *

+ * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object + * will enter the ERROR state. + *

+ * This method is executed on an internal executor service. + * + * @param playable The Playable object that is supposed to be played. This parameter must not be null. + * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via + * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by + * the Android MediaPlayer via getStreamUrl. + * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the + * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared + * for playback immediately (see 'prepareImmediately' parameter for more details) + * @param prepareImmediately Set to true if the method should also prepare the episode for playback. + */ + public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { + if (playable == null) + throw new IllegalArgumentException("playable = null"); + if (AppConfig.DEBUG) Log.d(TAG, "Play media object."); + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + try { + playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); + } catch (RuntimeException e) { + throw e; + } finally { + playerLock.unlock(); + } + } + }); + } + + /** + * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if + * the given playable parameter is the same object as the currently playing media. + *

+ * This method requires the playerLock and is executed on the caller's thread. + * + * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean) + */ + private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { + if (playable == null) + throw new IllegalArgumentException("playable = null"); + if (!playerLock.isHeldByCurrentThread()) + throw new IllegalStateException("method requires playerLock"); + + + if (media != null) { + if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { + // episode is already playing -> ignore method call + return; + } else { + // stop playback of this episode + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { + mediaPlayer.stop(); + } + setPlayerStatus(PlayerStatus.INDETERMINATE, null); + } + } + + this.media = playable; + this.stream = stream; + this.mediaType = media.getMediaType(); + this.videoSize = null; + createMediaPlayer(); + PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared); + setPlayerStatus(PlayerStatus.INITIALIZING, media); + try { + media.loadMetadata(); + if (stream) { + mediaPlayer.setDataSource(media.getStreamUrl()); + } else { + mediaPlayer.setDataSource(media.getLocalMediaUrl()); + } + setPlayerStatus(PlayerStatus.INITIALIZED, media); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT); + } + + if (prepareImmediately) { + setPlayerStatus(PlayerStatus.PREPARING, media); + mediaPlayer.prepare(); + onPrepared(startWhenPrepared); + } + + } catch (Playable.PlayableException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IllegalStateException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + + + /** + * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. + * nothing will happen. + *

+ * This method is executed on an internal executor service. + */ + public void resume() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + resumeSync(); + playerLock.unlock(); + } + }); + } + + private void resumeSync() { + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { + int focusGained = audioManager.requestAudioFocus( + audioFocusChangeListener, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + + setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); + mediaPlayer.start(); + if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + setPlayerStatus(PlayerStatus.PLAYING, media); + pausedBecauseOfTransientAudiofocusLoss = false; + if (android.os.Build.VERSION.SDK_INT >= 14) { + RemoteControlClient remoteControlClient = callback.getRemoteControlClient(); + if (remoteControlClient != null) { + audioManager + .registerRemoteControlClient(remoteControlClient); + } + } + audioManager + .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), + MediaButtonReceiver.class.getName())); + media.onPlaybackStart(); + + } else { + if (AppConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); + } + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); + } + } + + + /** + * Saves the current position and pauses playback. Note that, if audiofocus + * is abandoned, the lockscreen controls will also disapear. + *

+ * This method is executed on an internal executor service. + * + * @param abandonFocus is true if the service should release audio focus + * @param reinit is true if service should reinit after pausing if the media + * file is being streamed + */ + public void pause(final boolean abandonFocus, final boolean reinit) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus == PlayerStatus.PLAYING) { + if (AppConfig.DEBUG) + Log.d(TAG, "Pausing playback."); + mediaPlayer.pause(); + setPlayerStatus(PlayerStatus.PAUSED, media); + + if (abandonFocus) { + audioManager.abandonAudioFocus(audioFocusChangeListener); + pausedBecauseOfTransientAudiofocusLoss = false; + } + if (stream && reinit) { + reinit(); + } + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Prepared media player for playback if the service is in the INITALIZED + * state. + *

+ * This method is executed on an internal executor service. + */ + public void prepare() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus == PlayerStatus.INITIALIZED) { + if (AppConfig.DEBUG) + Log.d(TAG, "Preparing media player"); + setPlayerStatus(PlayerStatus.PREPARING, media); + try { + mediaPlayer.prepare(); + onPrepared(startWhenPrepared.get()); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + playerLock.unlock(); + + } + }); + } + + /** + * Called after media player has been prepared. This method is executed on the caller's thread. + */ + void onPrepared(final boolean startWhenPrepared) { + playerLock.lock(); + + if (playerStatus != PlayerStatus.PREPARING) { + playerLock.unlock(); + throw new IllegalStateException("Player is not in PREPARING state"); + } + + if (AppConfig.DEBUG) + Log.d(TAG, "Resource prepared"); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight()); + } + + if (media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + if (media.getDuration() == 0) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting duration of media"); + media.setDuration(mediaPlayer.getDuration()); + } + setPlayerStatus(PlayerStatus.PREPARED, media); + + if (startWhenPrepared) { + resumeSync(); + } + + playerLock.unlock(); + } + + /** + * Resets the media player and moves it into INITIALIZED state. + *

+ * This method is executed on an internal executor service. + */ + public void reinit() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (media != null) { + playMediaObject(media, true, stream, startWhenPrepared.get(), false); + } else if (mediaPlayer != null) { + mediaPlayer.reset(); + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + } + playerLock.unlock(); + } + }); + } + + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * Invalid time values (< 0) will be ignored. + *

+ * This method is executed on the caller's thread. + */ + private void seekToSync(int t) { + if (t < 0) { + if (AppConfig.DEBUG) Log.d(TAG, "Received invalid value for t"); + return; + } + playerLock.lock(); + + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + if (stream) { + // statusBeforeSeeking = playerStatus; + // setPlayerStatus(PlayerStatus.SEEKING, media); + } + mediaPlayer.seekTo(t); + + } else if (playerStatus == PlayerStatus.INITIALIZED) { + media.setPosition(t); + startWhenPrepared.set(true); + prepare(); + } + playerLock.unlock(); + } + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * Invalid time values (< 0) will be ignored. + *

+ * This method is executed on an internal executor service. + */ + public void seekTo(final int t) { + executor.submit(new Runnable() { + @Override + public void run() { + seekToSync(t); + } + }); + } + + /** + * Seek a specific position from the current position + * + * @param d offset from current position (positive or negative) + */ + public void seekDelta(final int d) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + int currentPosition = getPosition(); + if (currentPosition != INVALID_TIME) { + seekToSync(currentPosition + d); + } else { + Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Seek to the start of the specified chapter. + */ + public void seekToChapter(Chapter c) { + if (c == null) + throw new IllegalArgumentException("c = null"); + seekTo((int) c.getStart()); + } + + /** + * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. + */ + public int getDuration() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getDuration(); + } else if (media != null && media.getDuration() > 0) { + retVal = media.getDuration(); + } + + playerLock.unlock(); + return retVal; + } + + /** + * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. + */ + public int getPosition() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getCurrentPosition(); + } else if (media != null && media.getPosition() > 0) { + retVal = media.getPosition(); + } + + playerLock.unlock(); + return retVal; + } + + public boolean isStartWhenPrepared() { + return startWhenPrepared.get(); + } + + public void setStartWhenPrepared(boolean startWhenPrepared) { + this.startWhenPrepared.set(startWhenPrepared); + } + + /** + * Returns true if the playback speed can be adjusted. This method can also return false if the PSMP object's + * internal MediaPlayer cannot be accessed at the moment. + */ + public boolean canSetSpeed() { + if (!playerLock.tryLock()) { + return false; + } + boolean retVal = false; + if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { + retVal = (mediaPlayer).canSetSpeed(); + } + + playerLock.unlock(); + return retVal; + } + + /** + * Sets the playback speed. + * This method is executed on the caller's thread. + */ + private void setSpeedSync(float speed) { + playerLock.lock(); + if (media != null && media.getMediaType() == MediaType.AUDIO) { + if (mediaPlayer.canSetSpeed()) { + mediaPlayer.setPlaybackSpeed((float) speed); + if (AppConfig.DEBUG) + Log.d(TAG, "Playback speed was set to " + speed); + callback.playbackSpeedChanged(speed); + } + } + playerLock.unlock(); + } + + /** + * Sets the playback speed. + * This method is executed on an internal executor service. + */ + public void setSpeed(final float speed) { + executor.submit(new Runnable() { + @Override + public void run() { + setSpeedSync(speed); + } + }); + } + + /** + * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. + */ + public float getPlaybackSpeed() { + if (!playerLock.tryLock()) { + return 1; + } + + float retVal = 1; + if ((playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) { + retVal = mediaPlayer.getCurrentSpeedMultiplier(); + } + playerLock.unlock(); + return retVal; + } + + public MediaType getCurrentMediaType() { + return mediaType; + } + + public boolean isStreaming() { + return stream; + } + + + /** + * Releases internally used resources. This method should only be called when the object is not used anymore. + */ + public void shutdown() { + executor.shutdown(); + if (mediaPlayer != null) { + mediaPlayer.release(); + } + } + + public void setVideoSurface(final SurfaceHolder surface) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (mediaPlayer != null) { + mediaPlayer.setDisplay(surface); + } + playerLock.unlock(); + } + }); + } + + public void resetVideoSurface() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (AppConfig.DEBUG) + Log.d(TAG, "Resetting video surface"); + mediaPlayer.setDisplay(null); + reinit(); + playerLock.unlock(); + } + }); + } + + /** + * Return width and height of the currently playing video as a pair. + * + * @return Width and height as a Pair or null if the video size could not be determined. The method might still + * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return + * invalid values. + */ + public Pair getVideoSize() { + if (!playerLock.tryLock()) { + // use cached value if lock can't be aquired + return videoSize; + } + Pair res; + if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { + res = null; + } else { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight()); + res = videoSize; + } + playerLock.unlock(); + return res; + } + + /** + * Returns a PSMInfo object that contains information about the current state of the PSMP object. + * + * @return The PSMPInfo object. + */ + public synchronized PSMPInfo getPSMPInfo() { + return new PSMPInfo(playerStatus, media); + } + + /** + * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time + * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). + *

+ * This method will notify the callback about the change of the player status (even if the new status is the same + * as the old one). + * + * @param newStatus The new PlayerStatus. This must not be null. + * @param newMedia The new playable object of the PSMP object. This can be null. + */ + private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { + if (newStatus == null) + throw new IllegalArgumentException("newStatus = null"); + if (AppConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); + + this.playerStatus = newStatus; + this.media = newMedia; + callback.statusChanged(new PSMPInfo(playerStatus, media)); + } + + private IPlayer createMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.release(); + } + if (media == null || media.getMediaType() == MediaType.VIDEO) { + mediaPlayer = new VideoPlayer(); + } else { + mediaPlayer = new AudioPlayer(context); + } + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + return setMediaPlayerListeners(mediaPlayer); + } + + private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { + + @Override + public void onAudioFocusChange(final int focusChange) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus"); + pause(true, false); + callback.shouldStop(); + break; + case AudioManager.AUDIOFOCUS_GAIN: + if (AppConfig.DEBUG) + Log.d(TAG, "Gained audio focus"); + if (pausedBecauseOfTransientAudiofocusLoss) // we paused => play now + resume(); + else // we ducked => raise audio level back + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_RAISE, 0); + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (playerStatus == PlayerStatus.PLAYING) { + if (!UserPreferences.shouldPauseForFocusLoss()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, 0); + pausedBecauseOfTransientAudiofocusLoss = false; + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + if (playerStatus == PlayerStatus.PLAYING) { + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + + playerLock.unlock(); + } + }); + + } + }; + + public void endPlayback() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus != PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.INDETERMINATE, media); + } + if (mediaPlayer != null) { + mediaPlayer.reset(); + + } + callback.endPlayback(true); + + playerLock.unlock(); + } + }); + } + + /** + * Holds information about a PSMP object. + */ + public class PSMPInfo { + public PlayerStatus playerStatus; + public Playable playable; + + public PSMPInfo(PlayerStatus playerStatus, Playable playable) { + this.playerStatus = playerStatus; + this.playable = playable; + } + } + + public static interface PSMPCallback { + public void statusChanged(PSMPInfo newInfo); + + public void shouldStop(); + + public void playbackSpeedChanged(float s); + + public void onBufferingUpdate(int percent); + + public boolean onMediaPlayerInfo(int code); + + public boolean onMediaPlayerError(Object inObj, int what, int extra); + + public boolean endPlayback(boolean playNextEpisode); + + public RemoteControlClient getRemoteControlClient(); + } + + private IPlayer setMediaPlayerListeners(IPlayer mp) { + if (mp != null && media != null) { + if (media.getMediaType() == MediaType.AUDIO) { + ((AudioPlayer) mp) + .setOnCompletionListener(audioCompletionListener); + ((AudioPlayer) mp) + .setOnSeekCompleteListener(audioSeekCompleteListener); + ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); + ((AudioPlayer) mp) + .setOnBufferingUpdateListener(audioBufferingUpdateListener); + ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); + } else { + ((VideoPlayer) mp) + .setOnCompletionListener(videoCompletionListener); + ((VideoPlayer) mp) + .setOnSeekCompleteListener(videoSeekCompleteListener); + ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); + ((VideoPlayer) mp) + .setOnBufferingUpdateListener(videoBufferingUpdateListener); + ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); + } + } + return mp; + } + + private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(com.aocate.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(android.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private void genericOnCompletion() { + endPlayback(); + } + + private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, + int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private void genericOnBufferingUpdate(int percent) { + callback.onBufferingUpdate(percent); + } + + private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericInfoListener(what); + } + }; + + private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { + return genericInfoListener(what); + } + }; + + private boolean genericInfoListener(int what) { + return callback.onMediaPlayerInfo(what); + } + + private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericOnError(mp, what, extra); + } + }; + + private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(android.media.MediaPlayer mp, int what, int extra) { + return genericOnError(mp, what, extra); + } + }; + + private boolean genericOnError(Object inObj, int what, int extra) { + return callback.onMediaPlayerError(inObj, what, extra); + } + + private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(com.aocate.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(android.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final void genericSeekCompleteListener() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (playerStatus == PlayerStatus.SEEKING) { + setPlayerStatus(statusBeforeSeeking, media); + } + playerLock.unlock(); + } + }); + } +} diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java new file mode 100644 index 000000000..0c1878e18 --- /dev/null +++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java @@ -0,0 +1,385 @@ +package de.danoeh.antennapod.service.playback; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.EventDistributor; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.util.playback.Playable; + +import java.util.List; +import java.util.concurrent.*; + +/** + * Manages the background tasks of PlaybackSerivce, i.e. + * the sleep timer, the position saver, the widget updater and + * the queue loader. + *

+ * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) + * to notify the PlaybackService about updates from the running tasks. + */ +public class PlaybackServiceTaskManager { + private static final String TAG = "PlaybackServiceTaskManager"; + + /** + * Update interval of position saver in milliseconds. + */ + public static final int POSITION_SAVER_WAITING_INTERVAL = 5000; + /** + * Notification interval of widget updater in milliseconds. + */ + public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500; + + private static final int SCHED_EX_POOL_SIZE = 2; + private final ScheduledThreadPoolExecutor schedExecutor; + + private ScheduledFuture positionSaverFuture; + private ScheduledFuture widgetUpdaterFuture; + private ScheduledFuture sleepTimerFuture; + private volatile Future> queueFuture; + private volatile Future chapterLoaderFuture; + + private SleepTimer sleepTimer; + + private final Context context; + private final PSTMCallback callback; + + /** + * Sets up a new PSTM. This method will also start the queue loader task. + * + * @param context + * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. + */ + public PlaybackServiceTaskManager(Context context, PSTMCallback callback) { + if (context == null) + throw new IllegalArgumentException("context must not be null"); + if (callback == null) + throw new IllegalArgumentException("callback must not be null"); + + this.context = context; + this.callback = callback; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + loadQueue(); + EventDistributor.getInstance().register(eventDistributorListener); + } + + private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { + cancelQueueLoader(); + loadQueue(); + } + } + }; + + private synchronized boolean isQueueLoaderActive() { + return queueFuture != null && !queueFuture.isDone(); + } + + private synchronized void cancelQueueLoader() { + if (isQueueLoaderActive()) { + queueFuture.cancel(true); + } + } + + private synchronized void loadQueue() { + if (!isQueueLoaderActive()) { + queueFuture = schedExecutor.submit(new Callable>() { + @Override + public List call() throws Exception { + return DBReader.getQueue(context); + } + }); + } + } + + /** + * Returns the queue if it is already loaded or null if it hasn't been loaded yet. + * In order to wait until the queue has been loaded, use getQueue() + */ + public synchronized List getQueueIfLoaded() { + if (queueFuture.isDone()) { + try { + return queueFuture.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + return null; + } + + /** + * Returns the queue or waits until the PSTM has loaded the queue from the database. + */ + public synchronized List getQueue() throws InterruptedException { + try { + return queueFuture.get(); + } catch (ExecutionException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Starts the position saver task. If the position saver is already active, nothing will happen. + */ + public synchronized void startPositionSaver() { + if (!isPositionSaverActive()) { + Runnable positionSaver = new Runnable() { + @Override + public void run() { + callback.positionSaverTick(); + } + }; + positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, + POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); + + if (AppConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); + } + } + + /** + * Returns true if the position saver is currently running. + */ + public synchronized boolean isPositionSaverActive() { + return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone(); + } + + /** + * Cancels the position saver. If the position saver is not running, nothing will happen. + */ + public synchronized void cancelPositionSaver() { + if (isPositionSaverActive()) { + positionSaverFuture.cancel(false); + if (AppConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver"); + } + } + + /** + * Starts the widget updater task. If the widget updater is already active, nothing will happen. + */ + public synchronized void startWidgetUpdater() { + if (!isWidgetUpdaterActive()) { + Runnable widgetUpdater = new Runnable() { + @Override + public void run() { + callback.onWidgetUpdaterTick(); + } + }; + widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, + WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); + + if (AppConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater"); + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored."); + } + } + + /** + * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be + * cancelled first. + * After waitingTime has elapsed, onSleepTimerExpired() will be called. + * + * @throws java.lang.IllegalArgumentException if waitingTime <= 0 + */ + public synchronized void setSleepTimer(long waitingTime) { + if (waitingTime <= 0) + throw new IllegalArgumentException("waitingTime <= 0"); + + if (AppConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + if (isSleepTimerActive()) { + sleepTimerFuture.cancel(true); + } + sleepTimer = new SleepTimer(waitingTime); + sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); + } + + /** + * Returns true if the sleep timer is currently active. + */ + public synchronized boolean isSleepTimerActive() { + return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting; + } + + /** + * Disables the sleep timer. If the sleep timer is not active, nothing will happen. + */ + public synchronized void disableSleepTimer() { + if (isSleepTimerActive()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Disabling sleep timer"); + sleepTimerFuture.cancel(true); + } + } + + /** + * Returns the current sleep timer time or 0 if the sleep timer is not active. + */ + public synchronized long getSleepTimerTimeLeft() { + if (isSleepTimerActive()) { + return sleepTimer.getWaitingTime(); + } else { + return 0; + } + } + + + /** + * Returns true if the widget updater is currently running. + */ + public synchronized boolean isWidgetUpdaterActive() { + return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone(); + } + + /** + * Cancels the widget updater. If the widget updater is not running, nothing will happen. + */ + public synchronized void cancelWidgetUpdater() { + if (isWidgetUpdaterActive()) { + widgetUpdaterFuture.cancel(false); + if (AppConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater"); + } + } + + private synchronized void cancelChapterLoader() { + if (isChapterLoaderActive()) { + chapterLoaderFuture.cancel(true); + } + } + + private synchronized boolean isChapterLoaderActive() { + return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); + } + + /** + * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, + * it will be cancelled first. + * On completion, the callback's onChapterLoaded method will be called. + */ + public synchronized void startChapterLoader(final Playable media) { + if (media == null) + throw new IllegalArgumentException("media = null"); + + if (isChapterLoaderActive()) { + cancelChapterLoader(); + } + + Runnable chapterLoader = new Runnable() { + @Override + public void run() { + if (AppConfig.DEBUG) + Log.d(TAG, "Chapter loader started"); + if (media.getChapters() == null) { + media.loadChapterMarks(); + if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) { + callback.onChapterLoaded(media); + } + } + if (AppConfig.DEBUG) + Log.d(TAG, "Chapter loader stopped"); + } + }; + chapterLoaderFuture = schedExecutor.submit(chapterLoader); + } + + + /** + * Cancels all tasks. The PSTM will be in the initial state after execution of this method. + */ + public synchronized void cancelAllTasks() { + cancelPositionSaver(); + cancelWidgetUpdater(); + disableSleepTimer(); + cancelQueueLoader(); + cancelChapterLoader(); + } + + /** + * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after + * execution of this method. + */ + public synchronized void shutdown() { + EventDistributor.getInstance().unregister(eventDistributorListener); + cancelAllTasks(); + schedExecutor.shutdown(); + } + + /** + * Sleeps for a given time and then pauses playback. + */ + private class SleepTimer implements Runnable { + private static final String TAG = "SleepTimer"; + private static final long UPDATE_INTERVALL = 1000L; + private volatile long waitingTime; + private volatile boolean isWaiting; + + public SleepTimer(long waitingTime) { + super(); + this.waitingTime = waitingTime; + isWaiting = true; + } + + @Override + public void run() { + if (AppConfig.DEBUG) + Log.d(TAG, "Starting"); + while (waitingTime > 0) { + try { + Thread.sleep(UPDATE_INTERVALL); + waitingTime -= UPDATE_INTERVALL; + + if (waitingTime <= 0) { + if (AppConfig.DEBUG) + Log.d(TAG, "Waiting completed"); + postExecute(); + if (!Thread.currentThread().isInterrupted()) { + callback.onSleepTimerExpired(); + } + + } + } catch (InterruptedException e) { + Log.d(TAG, "Thread was interrupted while waiting"); + break; + } + } + postExecute(); + } + + protected void postExecute() { + isWaiting = false; + } + + public long getWaitingTime() { + return waitingTime; + } + + public boolean isWaiting() { + return isWaiting; + } + + } + + public static interface PSTMCallback { + void positionSaverTick(); + + void onSleepTimerExpired(); + + void onWidgetUpdaterTick(); + + void onChapterLoaded(Playable media); + } +} diff --git a/src/de/danoeh/antennapod/service/PlayerStatus.java b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java similarity index 58% rename from src/de/danoeh/antennapod/service/PlayerStatus.java rename to src/de/danoeh/antennapod/service/playback/PlayerStatus.java index fbf5b1505..3d2b4ad39 100644 --- a/src/de/danoeh/antennapod/service/PlayerStatus.java +++ b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java @@ -1,14 +1,14 @@ -package de.danoeh.antennapod.service; +package de.danoeh.antennapod.service.playback; public enum PlayerStatus { + INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state. ERROR, PREPARING, PAUSED, PLAYING, STOPPED, PREPARED, - SEEKING, - AWAITING_VIDEO_SURFACE, // player has been initialized and the media type to be played is a video. + SEEKING, INITIALIZING, // playback service is loading the Playable's metadata INITIALIZED // playback service was started, data source of media player was set. } diff --git a/src/de/danoeh/antennapod/service/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java similarity index 88% rename from src/de/danoeh/antennapod/service/PlayerWidgetService.java rename to src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java index 475af9655..90ad7a9fa 100644 --- a/src/de/danoeh/antennapod/service/PlayerWidgetService.java +++ b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.service; +package de.danoeh.antennapod.service.playback; import android.app.PendingIntent; import android.app.Service; @@ -6,6 +6,7 @@ import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; +import android.os.Build; import android.os.IBinder; import android.util.Log; import android.view.KeyEvent; @@ -72,9 +73,11 @@ public class PlayerWidgetService extends Service { } private void updateViews() { + if (playbackService == null) { + return; + } isUpdating = true; - if (AppConfig.DEBUG) - Log.d(TAG, "Updating widget views"); + ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); AppWidgetManager manager = AppWidgetManager.getInstance(this); RemoteViews views = new RemoteViews(getPackageName(), @@ -83,8 +86,8 @@ public class PlayerWidgetService extends Service { PlaybackService.getPlayerActivityIntent(this), 0); views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer); - if (playbackService != null && playbackService.getMedia() != null) { - Playable media = playbackService.getMedia(); + final Playable media = playbackService.getPlayable(); + if (playbackService != null && media != null) { PlayerStatus status = playbackService.getStatus(); views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); @@ -95,14 +98,18 @@ public class PlayerWidgetService extends Service { views.setTextViewText(R.id.txtvProgress, progressString); } views.setImageViewResource(R.id.butPlay, R.drawable.av_pause_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); + } } else { views.setImageViewResource(R.id.butPlay, R.drawable.av_play_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); + } } views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent()); } else { - if (AppConfig.DEBUG) - Log.d(TAG, "No media playing. Displaying defaultt views"); views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE); views.setTextViewText(R.id.txtvTitle, this.getString(R.string.no_media_playing_label)); @@ -126,8 +133,8 @@ public class PlayerWidgetService extends Service { } private String getProgressString(PlaybackService ps) { - int position = ps.getCurrentPositionSafe(); - int duration = ps.getDurationSafe(); + int position = ps.getCurrentPosition(); + int duration = ps.getDuration(); if (position != PlaybackService.INVALID_TIME && duration != PlaybackService.INVALID_TIME) { return Converter.getDurationStringLong(position) + " / " diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java index 8aa93d7ed..ccbf6646f 100644 --- a/src/de/danoeh/antennapod/storage/DBReader.java +++ b/src/de/danoeh/antennapod/storage/DBReader.java @@ -1,20 +1,23 @@ package de.danoeh.antennapod.storage; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.service.download.*; +import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.comparator.DownloadStatusComparator; import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; +import de.danoeh.antennapod.util.comparator.PlaybackCompletionDateComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; /** * Provides methods for reading data from the AntennaPod database. @@ -123,6 +126,7 @@ public final class DBReader { * Takes a list of FeedItems and loads their corresponding Feed-objects from the database. * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will * not find the correct feed of an item. + * * @param context A context that is used for opening a database connection. * @param items The FeedItems whose Feed-objects should be loaded. */ @@ -210,7 +214,9 @@ public final class DBReader { .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0)); item.setItemIdentifier(itemlistCursor .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); - + item.setFlattrStatus(new FlattrStatus(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS))); + // extract chapters boolean hasSimpleChapters = itemlistCursor .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0; @@ -301,7 +307,8 @@ public final class DBReader { cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, - playbackCompletionDate); + playbackCompletionDate, + cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX)); } private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, @@ -329,7 +336,8 @@ public final class DBReader { image, cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL), cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0); + cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0, + new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS))); if (image != null) { image.setFeed(feed); @@ -515,8 +523,9 @@ public final class DBReader { List items = extractItemlistFromCursor(adapter, itemCursor); loadFeedDataOfFeedItemlist(context, items); itemCursor.close(); - adapter.close(); + + Collections.sort(items, new PlaybackCompletionDateComparator()); return items; } @@ -774,4 +783,48 @@ public final class DBReader { return media; } + + /** + * Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems. + * + * @param context A context that is used for opening a database connection. + * @return The flattr queue as a List. + */ + public static List getFlattrQueue(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List result = new ArrayList(); + + // load feeds + Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor(); + if (feedCursor.moveToFirst()) { + do { + result.add(extractFeedFromCursorRow(adapter, feedCursor)); + } while (feedCursor.moveToNext()); + } + feedCursor.close(); + + //load feed items + Cursor feedItemCursor = adapter.getFeedItemsInFlattrQueueCursor(); + result.addAll(extractItemlistFromCursor(adapter, feedItemCursor)); + feedItemCursor.close(); + + adapter.close(); + Log.d(TAG, "Returning flattrQueueIterator for queue with " + result.size() + " items."); + return result; + } + + + /** + * Returns true if the flattr queue is empty. + * + * @param context A context that is used for opening a database connection. + */ + public static boolean getFlattrQueueEmpty(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + boolean empty = adapter.getFlattrQueueSize() == 0; + adapter.close(); + return empty; + } } diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java index 26d5c712a..8ef5f0888 100644 --- a/src/de/danoeh/antennapod/storage/DBTasks.java +++ b/src/de/danoeh/antennapod/storage/DBTasks.java @@ -4,17 +4,20 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.util.Log; +import de.danoeh.antennapod.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.asynctask.FlattrStatusFetcher; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.GpodnetSyncService; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.NetworkUtils; import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.util.exception.MediaFileNotFoundException; +import de.danoeh.antennapod.util.flattr.FlattrUtils; import java.util.*; import java.util.concurrent.*; @@ -151,6 +154,12 @@ public final class DBTasks { } isRefreshing.set(false); + if (AppConfig.DEBUG) Log.d(TAG, "Flattring all pending things."); + new FlattrClickWorker(context, FlattrClickWorker.FLATTR_NOTIFICATION).executeSync(); // flattr pending things + + if (AppConfig.DEBUG) Log.d(TAG, "Fetching flattr status."); + new FlattrStatusFetcher(context).start(); + GpodnetSyncService.sendSyncIntent(context); autodownloadUndownloadedItems(context); } @@ -797,4 +806,34 @@ public final class DBTasks { } } + /** + * Adds the given FeedItem to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * @param context + * @param item + */ + public static void flattrItemIfLoggedIn(Context context, FeedItem item) { + if (FlattrUtils.hasToken()) { + item.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, item, true); + } else { + FlattrUtils.showNoTokenDialog(context, item.getPaymentLink()); + } + } + + /** + * Adds the given Feed to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * @param context + * @param feed + */ + public static void flattrFeedIfLoggedIn(Context context, Feed feed) { + if (FlattrUtils.hasToken()) { + feed.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, feed, true); + } else { + FlattrUtils.showNoTokenDialog(context, feed.getPaymentLink()); + } + } + } diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java index 6be1a5327..444e9ea0c 100644 --- a/src/de/danoeh/antennapod/storage/DBWriter.java +++ b/src/de/danoeh/antennapod/storage/DBWriter.java @@ -1,15 +1,5 @@ package de.danoeh.antennapod.storage; -import java.io.File; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; - import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -17,13 +7,29 @@ import android.database.Cursor; import android.preference.PreferenceManager; import android.util.Log; import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.preferences.GpodnetPreferences; import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.service.GpodnetSyncService; -import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; +import de.danoeh.antennapod.util.flattr.SimpleFlattrThing; +import org.shredzone.flattr4j.model.Flattr; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; /** * Provides methods for writing data to AntennaPod's database. @@ -222,6 +228,9 @@ public class DBWriter { if (AppConfig.DEBUG) Log.d(TAG, "Adding new item to playback history"); media.setPlaybackCompletionDate(new Date()); + // reset played_duration to 0 so that it behaves correctly when the episode is played again + media.setPlayedDuration(0); + PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); adapter.setFeedMediaPlaybackCompletionDate(media); @@ -446,7 +455,7 @@ public class DBWriter { }); } - + /** * Moves the specified item to the top of the queue. * @@ -472,7 +481,7 @@ public class DBWriter { } }); } - + /** * Moves the specified item to the bottom of the queue. * @@ -491,7 +500,7 @@ public class DBWriter { for (long id : queueIdList) { if (id == itemId) { moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1, - broadcastUpdate); + broadcastUpdate); return; } currentLocation++; @@ -500,7 +509,7 @@ public class DBWriter { } }); } - + /** * Changes the position of a FeedItem in the queue. * @@ -524,7 +533,7 @@ public class DBWriter { /** * Changes the position of a FeedItem in the queue. - * + *

* This function must be run using the ExecutorService (dbExec). * * @param context A context that is used for opening a database connection. @@ -535,7 +544,7 @@ public class DBWriter { * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) */ private static void moveQueueItemHelper(final Context context, final int from, - final int to, final boolean broadcastUpdate) { + final int to, final boolean broadcastUpdate) { final PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); final List queue = DBReader @@ -823,4 +832,125 @@ public class DBWriter { } return false; } + + /** + * Saves the FlattrStatus of a FeedItem object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + public static Future setFeedItemFlattrStatus(final Context context, + final FeedItem item, + final boolean startFlattrClickWorker) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemFlattrStatus(item); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync(); + } + } + }); + } + + /** + * Saves the FlattrStatus of a Feed object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + private static Future setFeedFlattrStatus(final Context context, + final Feed feed, + final boolean startFlattrClickWorker) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedFlattrStatus(feed); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync(); + } + } + }); + } + + /** + * format an url for querying the database + * (postfix a / and apply percent-encoding) + */ + private static String formatURIForQuery(String uri) { + try { + return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getMessage()); + return ""; + } + } + + + /** + * Set flattr status of the passed thing (either a FeedItem or a Feed) + * + * @param context + * @param thing + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + * @return + */ + public static Future setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) { + // must propagate this to back db + if (thing instanceof FeedItem) + return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker); + else if (thing instanceof Feed) + return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker); + else if (thing instanceof SimpleFlattrThing) { + } // SimpleFlattrThings are generated on the fly and do not have DB backing + else + Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing"); + + return null; + } + + /** + * Reset flattr status to unflattrd for all items + */ + public static Future clearAllFlattrStatus(final Context context) { + Log.d(TAG, "clearAllFlattrStatus()"); + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearAllFlattrStatus(); + adapter.close(); + } + }); + } + + /** + * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp, + * where the information has been retrieved from the flattr API + */ + public static Future setFlattredStatus(final Context context, final List flattrList) { + Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items"); + // clear flattr status in db + clearAllFlattrStatus(context); + + // submit list with flattred things having normalized URLs to db + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (Flattr flattr : flattrList) { + adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime())); + } + adapter.close(); + } + }); + } } diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index 068f80ded..b44883744 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -1,8 +1,5 @@ package de.danoeh.antennapod.storage; -import java.util.Arrays; -import java.util.List; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -16,6 +13,10 @@ import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.util.flattr.FlattrStatus; + +import java.util.Arrays; +import java.util.List; // TODO Remove media column from feeditem table @@ -24,7 +25,7 @@ import de.danoeh.antennapod.service.download.DownloadStatus; */ public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; - private static final int DATABASE_VERSION = 10; + private static final int DATABASE_VERSION = 11; public static final String DATABASE_NAME = "Antennapod.db"; /** @@ -54,6 +55,7 @@ public class PodDBAdapter { public static final int KEY_IMAGE_INDEX = 11; public static final int KEY_TYPE_INDEX = 12; public static final int KEY_FEED_IDENTIFIER_INDEX = 13; + public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14; // ----------- FeedItem indices public static final int KEY_CONTENT_ENCODED_INDEX = 2; public static final int KEY_PUBDATE_INDEX = 3; @@ -62,6 +64,7 @@ public class PodDBAdapter { public static final int KEY_FEED_INDEX = 9; public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; + public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12; // ---------- FeedMedia indices public static final int KEY_DURATION_INDEX = 1; public static final int KEY_POSITION_INDEX = 5; @@ -69,6 +72,7 @@ public class PodDBAdapter { public static final int KEY_MIME_TYPE_INDEX = 7; public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; public static final int KEY_MEDIA_FEEDITEM_INDEX = 9; + public static final int KEY_PLAYED_DURATION_INDEX = 10; // --------- Download log indices public static final int KEY_FEEDFILE_INDEX = 1; public static final int KEY_FEEDFILETYPE_INDEX = 2; @@ -119,12 +123,14 @@ public class PodDBAdapter { public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; public static final String KEY_TYPE = "type"; public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; + public static final String KEY_FLATTR_STATUS = "flattr_status"; public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; public static final String KEY_REASON_DETAILED = "reason_detailed"; public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; public static final String KEY_CHAPTER_TYPE = "type"; public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; public static final String KEY_AUTO_DOWNLOAD = "auto_download"; + public static final String KEY_PLAYED_DURATION = "played_duration"; // Table names public static final String TABLE_NAME_FEEDS = "Feeds"; @@ -146,7 +152,8 @@ public class PodDBAdapter { + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," - + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1)"; + + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," + + KEY_FLATTR_STATUS + " INTEGER)"; private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -154,7 +161,8 @@ public class PodDBAdapter { + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," - + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT)"; + + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + + KEY_FLATTR_STATUS + " INTEGER)"; private static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE " + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -167,7 +175,8 @@ public class PodDBAdapter { + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," - + KEY_FEEDITEM + " INTEGER)"; + + KEY_FEEDITEM + " INTEGER," + + KEY_PLAYED_DURATION + " INTEGER)"; private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE @@ -208,6 +217,7 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_TYPE, TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, + TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS }; // column indices for FEED_SEL_STD @@ -226,6 +236,7 @@ public class PodDBAdapter { public static final int IDX_FEED_SEL_STD_TYPE = 12; public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13; public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14; + public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15; /** @@ -241,7 +252,8 @@ public class PodDBAdapter { TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA, TABLE_NAME_FEED_ITEMS + "." + KEY_FEED, TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, - TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER}; + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER, + TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS}; /** * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries. @@ -265,6 +277,7 @@ public class PodDBAdapter { public static final int IDX_FI_SMALL_FEED = 7; public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8; public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9; + public static final int IDX_FI_SMALL_FLATTR_STATUS = 10; /** * Select id, description and content-encoded column from feeditems. @@ -346,6 +359,10 @@ public class PodDBAdapter { values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime()); values.put(KEY_TYPE, feed.getType()); values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); + + Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong()); + + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); if (feed.getId() == 0) { // Create new entry if (AppConfig.DEBUG) @@ -435,6 +452,7 @@ public class PodDBAdapter { ContentValues values = new ContentValues(); values.put(KEY_POSITION, media.getPosition()); values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(media.getId())}); } else { @@ -446,6 +464,7 @@ public class PodDBAdapter { if (media.getId() != 0) { ContentValues values = new ContentValues(); values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(media.getId())}); } else { @@ -469,6 +488,56 @@ public class PodDBAdapter { db.endTransaction(); } + /** + * Update the flattr status of a feed + */ + public void setFeedFlattrStatus(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); + } + + /** + * Get all feeds in the flattr queue. + */ + public Cursor getFeedsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)},null, null, null); + } + + /** + * Get all feed items in the flattr queue. + */ + public Cursor getFeedItemsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)},null, null, null); + } + + /** + * Counts feeds and feed items in the flattr queue + */ + public int getFlattrQueueSize() { + int res = 0; + Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res = c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds"); + } + c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res += c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items"); + } + + return res; + } + /** * Updates the download URL of a Feed. */ @@ -495,6 +564,63 @@ public class PodDBAdapter { return result; } + /** + * Update the flattr status of a FeedItem + */ + public void setFeedItemFlattrStatus(FeedItem feedItem) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feedItem.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(feedItem.getId())}); + } + + /** + * Update the flattr status of a feed or feed item specified by its payment link + * and the new flattr status to use + */ + public void setItemFlattrStatus(String url, FlattrStatus status) + { + //Log.d(TAG, "setItemFlattrStatus(" + url + ") = " + status.toString()); + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, status.toLong()); + + // regexps in sqlite would be neat! + String[] query_urls = new String[]{ + "*" + url + "&*", + "*" + url + "%2F&*", + "*" + url + "", + "*" + url + "%2F" + }; + + if (db.update(TABLE_NAME_FEEDS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls) > 0) + { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in Feeds table"); + return; + } + if (db.update(TABLE_NAME_FEED_ITEMS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls) > 0) + { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in FeedsItems table"); + } + } + + /** + * Reset flattr status to unflattrd for all items + */ + public void clearAllFlattrStatus() + { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, 0); + db.update(TABLE_NAME_FEEDS, values, null, null); + db.update(TABLE_NAME_FEED_ITEMS, values, null, null); + } + /** * Inserts or updates a feeditem entry * @@ -522,6 +648,7 @@ public class PodDBAdapter { values.put(KEY_READ, item.isRead()); values.put(KEY_HAS_CHAPTERS, item.getChapters() != null); values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); + values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong()); if (item.getId() == 0) { item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); } else { @@ -711,7 +838,7 @@ public class PodDBAdapter { */ public final Cursor getAllFeedsCursor() { Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null, - KEY_TITLE + " ASC"); + KEY_TITLE + " COLLATE NOCASE ASC"); return c; } @@ -849,7 +976,7 @@ public class PodDBAdapter { /** * Returns a cursor which contains feed media objects with a playback - * completion date in descending order. + * completion date in ascending order. * * @param limit The maximum row count of the returned cursor. Must be an * integer >= 0. @@ -860,8 +987,8 @@ public class PodDBAdapter { throw new IllegalArgumentException("Limit must be >= 0"); } Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, - KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null, - null, KEY_PLAYBACK_COMPLETION_DATE + " DESC LIMIT " + limit); + KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null, + null, null); return c; } @@ -1072,7 +1199,7 @@ public class PodDBAdapter { " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + - " ON Feeds.id = feed ORDER BY Feeds.title;"; + " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;"; public Cursor getFeedStatisticsCursor() { return db.rawQuery(FEED_STATISTICS_QUERY, null); @@ -1170,6 +1297,17 @@ public class PodDBAdapter { + " ADD COLUMN " + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1"); } + if (oldVersion <= 10) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_PLAYED_DURATION + + " INTEGER"); + } } } } diff --git a/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java b/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java index 5a2c6005e..3eb49172d 100644 --- a/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java +++ b/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java @@ -101,7 +101,10 @@ public class NSRSS20 extends Namespace { } if (top.equals(GUID) && second.equals(ITEM)) { - state.getCurrentItem().setItemIdentifier(content); + // some feed creators include an empty or non-standard guid-element in their feed, which should be ignored + if (!content.isEmpty()) { + state.getCurrentItem().setItemIdentifier(content); + } } else if (top.equals(TITLE)) { if (second.equals(ITEM)) { state.getCurrentItem().setTitle(content); diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java b/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java new file mode 100644 index 000000000..a1d6d3bc4 --- /dev/null +++ b/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.util.flattr; + +import java.util.Calendar; + +public class FlattrStatus { + public static final int STATUS_UNFLATTERED = 0; + public static final int STATUS_QUEUE = 1; + public static final int STATUS_FLATTRED = 2; + + private int status = STATUS_UNFLATTERED; + private Calendar lastFlattred; + + public FlattrStatus() { + status = STATUS_UNFLATTERED; + lastFlattred = Calendar.getInstance(); + } + + public FlattrStatus(long status) { + lastFlattred = Calendar.getInstance(); + fromLong(status); + } + + public void setFlattred() { + status = STATUS_FLATTRED; + lastFlattred = Calendar.getInstance(); + } + + public void setUnflattred() { + status = STATUS_UNFLATTERED; + } + + public boolean getUnflattred() { + return status == STATUS_UNFLATTERED; + } + + public void setFlattrQueue() { + if (flattrable()) + status = STATUS_QUEUE; + } + + public void fromLong(long status) { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + this.status = (int) status; + else { + this.status = STATUS_FLATTRED; + lastFlattred.setTimeInMillis(status); + } + } + + public long toLong() { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + return status; + else { + return lastFlattred.getTimeInMillis(); + } + } + + public boolean flattrable() { + Calendar firstOfMonth = Calendar.getInstance(); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) ); + } + + public boolean getFlattrQueue() { + return status == STATUS_QUEUE; + } +} diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrThing.java b/src/de/danoeh/antennapod/util/flattr/FlattrThing.java new file mode 100644 index 000000000..872132517 --- /dev/null +++ b/src/de/danoeh/antennapod/util/flattr/FlattrThing.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.util.flattr; + +import de.danoeh.antennapod.util.flattr.FlattrStatus; + +public interface FlattrThing { + public String getTitle(); + public String getPaymentLink(); + public FlattrStatus getFlattrStatus(); +} diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java b/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java index ca2c9eb0f..215e67e55 100644 --- a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java +++ b/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java @@ -1,9 +1,16 @@ package de.danoeh.antennapod.util.flattr; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; import java.util.EnumSet; +import java.util.List; +import java.util.ListIterator; +import java.util.TimeZone; import org.shredzone.flattr4j.FlattrService; import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; import org.shredzone.flattr4j.model.Thing; import org.shredzone.flattr4j.oauth.AccessToken; import org.shredzone.flattr4j.oauth.AndroidAuthenticator; @@ -23,6 +30,7 @@ import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FlattrAuthActivity; import de.danoeh.antennapod.asynctask.FlattrTokenFetcher; +import de.danoeh.antennapod.storage.DBWriter; /** Utility methods for doing something with flattr. */ @@ -119,6 +127,58 @@ public class FlattrUtils { Log.e(TAG, "clickUrl was called with null access token"); } } + + public static List retrieveFlattredThings() + throws FlattrException { + ArrayList myFlattrs = new ArrayList(); + + if (hasToken()) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + + Calendar firstOfMonth = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + firstOfMonth.set(Calendar.MILLISECOND, 0); + firstOfMonth.set(Calendar.SECOND, 0); + firstOfMonth.set(Calendar.MINUTE, 0); + firstOfMonth.set(Calendar.HOUR_OF_DAY, 0); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + Date firstOfMonthDate = firstOfMonth.getTime(); + + // subscriptions some times get flattrd slightly before midnight - give it an hour leeway + firstOfMonthDate = new Date(firstOfMonthDate.getTime() - 60*60*1000); + + final int FLATTR_COUNT = 30; + final int FLATTR_MAXPAGE = 5; + + int page = 0; + do { + myFlattrs.ensureCapacity(FLATTR_COUNT*(page+1)); + + for (Flattr fl: fs.getMyFlattrs(FLATTR_COUNT, page)) { + if (fl.getCreated().after(firstOfMonthDate)) + myFlattrs.add(fl); + else + break; + } + page++; + } + while (myFlattrs.get(myFlattrs.size()-1).getCreated().after( firstOfMonthDate ) && page < FLATTR_MAXPAGE); + + if (AppConfig.DEBUG) { + Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate); + + for (Flattr fl: myFlattrs) { + Thing thing = fl.getThing(); + Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated()); + } + } + + } else { + Log.e(TAG, "retrieveFlattrdThings was called with null access token"); + } + + return myFlattrs; + } public static void handleCallback(Context context, Uri uri) { AndroidAuthenticator auth = createAuthenticator(); @@ -131,7 +191,8 @@ public class FlattrUtils { deleteToken(); FlattrServiceCreator.deleteFlattrService(); showRevokeDialog(context); - } + DBWriter.clearAllFlattrStatus(context); + } // ------------------------------------------------ DIALOGS diff --git a/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java b/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java new file mode 100644 index 000000000..296610871 --- /dev/null +++ b/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.util.flattr; + +/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */ +public class SimpleFlattrThing implements FlattrThing { + public SimpleFlattrThing(String title, String url, FlattrStatus status) + { + this.title = title; + this.url = url; + this.status = status; + } + + public String getTitle() + { + return this.title; + } + + public String getPaymentLink() + { + return this.url; + } + + public FlattrStatus getFlattrStatus() + { + return this.status; + } + + private String title; + private String url; + private FlattrStatus status; +} diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java index e99a733dc..615c1c93e 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java @@ -5,9 +5,8 @@ import android.content.Intent; import android.net.Uri; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; @@ -15,161 +14,158 @@ import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.ShareUtils; -import java.util.List; - -/** Handles interactions with the FeedItemMenu. */ +/** + * Handles interactions with the FeedItemMenu. + */ public class FeedItemMenuHandler { - private FeedItemMenuHandler() { + private static final String TAG = "FeedItemMenuHandler"; - } + private FeedItemMenuHandler() { - /** - * Used by the MenuHandler to access different types of menus through one - * interface - */ - public interface MenuInterface { - /** - * Implementations of this method should call findItem(id) on their - * menu-object and call setVisibility(visibility) on the returned - * MenuItem object. - */ - abstract void setItemVisibility(int id, boolean visible); - } + } - /** - * This method should be called in the prepare-methods of menus. It changes - * the visibility of the menu items depending on a FeedItem's attributes. - * - * @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 - * @return Returns true if selectedItem is not null. - * */ - public static boolean onPrepareMenu(MenuInterface mi, - FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { + /** + * Used by the MenuHandler to access different types of menus through one + * interface + */ + public interface MenuInterface { + /** + * Implementations of this method should call findItem(id) on their + * menu-object and call setVisibility(visibility) on the returned + * MenuItem object. + */ + abstract void setItemVisibility(int id, boolean visible); + } + + /** + * This method should be called in the prepare-methods of menus. It changes + * the visibility of the menu items depending on a FeedItem's attributes. + * + * @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 + * @return Returns true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { if (selectedItem == null) { return false; } - DownloadRequester requester = DownloadRequester.getInstance(); - boolean hasMedia = selectedItem.getMedia() != null; - boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); - boolean downloading = hasMedia - && requester.isDownloadingFile(selectedItem.getMedia()); - boolean notLoadedAndNotLoading = hasMedia && (!downloaded) - && (!downloading); - boolean isPlaying = hasMedia - && selectedItem.getState() == FeedItem.State.PLAYING; + DownloadRequester requester = DownloadRequester.getInstance(); + boolean hasMedia = selectedItem.getMedia() != null; + boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); + boolean downloading = hasMedia + && requester.isDownloadingFile(selectedItem.getMedia()); + boolean notLoadedAndNotLoading = hasMedia && (!downloaded) + && (!downloading); + boolean isPlaying = hasMedia + && selectedItem.getState() == FeedItem.State.PLAYING; - FeedItem.State state = selectedItem.getState(); + FeedItem.State state = selectedItem.getState(); - if (!isPlaying) { - mi.setItemVisibility(R.id.skip_episode_item, false); - } - if (!downloaded || isPlaying) { - mi.setItemVisibility(R.id.play_item, false); - mi.setItemVisibility(R.id.remove_item, false); - } - if (!notLoadedAndNotLoading) { - mi.setItemVisibility(R.id.download_item, false); - } - if (!(notLoadedAndNotLoading | downloading) | isPlaying) { - mi.setItemVisibility(R.id.stream_item, false); - } - if (!downloading) { - mi.setItemVisibility(R.id.cancel_download_item, false); - } + if (!isPlaying) { + mi.setItemVisibility(R.id.skip_episode_item, false); + } + if (!downloaded || isPlaying) { + mi.setItemVisibility(R.id.play_item, false); + mi.setItemVisibility(R.id.remove_item, false); + } + if (!notLoadedAndNotLoading) { + mi.setItemVisibility(R.id.download_item, false); + } + if (!(notLoadedAndNotLoading | downloading) | isPlaying) { + mi.setItemVisibility(R.id.stream_item, false); + } + if (!downloading) { + mi.setItemVisibility(R.id.cancel_download_item, false); + } - boolean isInQueue = queueAccess.contains(selectedItem.getId()); - if (!isInQueue || isPlaying) { - mi.setItemVisibility(R.id.remove_from_queue_item, false); - } - if (!(!isInQueue && selectedItem.getMedia() != null)) { - mi.setItemVisibility(R.id.add_to_queue_item, false); - } - if (!showExtendedMenu || selectedItem.getLink() == null) { - mi.setItemVisibility(R.id.share_link_item, false); - } + boolean isInQueue = queueAccess.contains(selectedItem.getId()); + if (!isInQueue || isPlaying) { + mi.setItemVisibility(R.id.remove_from_queue_item, false); + } + if (!(!isInQueue && selectedItem.getMedia() != null)) { + mi.setItemVisibility(R.id.add_to_queue_item, false); + } + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.share_link_item, false); + } - if (!AppConfig.DEBUG - || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { - mi.setItemVisibility(R.id.mark_unread_item, false); - } - if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { - mi.setItemVisibility(R.id.mark_read_item, false); - } + if (!AppConfig.DEBUG + || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { + mi.setItemVisibility(R.id.mark_unread_item, false); + } + if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { + mi.setItemVisibility(R.id.mark_read_item, false); + } - if (!showExtendedMenu || selectedItem.getLink() == null) { - mi.setItemVisibility(R.id.visit_website_item, false); - } + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.visit_website_item, false); + } - if (selectedItem.getPaymentLink() == null) { - mi.setItemVisibility(R.id.support_item, false); - } - return true; - } + if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) { + mi.setItemVisibility(R.id.support_item, false); + } + return true; + } - public static boolean onMenuItemClicked(Context context, int menuItemId, - FeedItem selectedItem) throws DownloadRequestException { - DownloadRequester requester = DownloadRequester.getInstance(); - switch (menuItemId) { - case R.id.skip_episode_item: - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); - break; - case R.id.download_item: - DBTasks.downloadFeedItems(context, selectedItem); - break; - case R.id.play_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - false); - break; - case R.id.remove_item: - DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); - break; - case R.id.cancel_download_item: - requester.cancelDownload(context, selectedItem.getMedia()); - break; - case R.id.mark_read_item: - DBWriter.markItemRead(context, selectedItem, true, true); - break; - case R.id.mark_unread_item: - DBWriter.markItemRead(context, selectedItem, false, true); - break; - case R.id.add_to_queue_item: - DBWriter.addQueueItem(context, selectedItem.getId()); - break; - case R.id.remove_from_queue_item: - DBWriter.removeQueueItem(context, selectedItem.getId(), true); - break; - case R.id.stream_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - true); - break; - case R.id.visit_website_item: - Uri uri = Uri.parse(selectedItem.getLink()); - context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - break; - case R.id.support_item: - new FlattrClickWorker(context, selectedItem.getPaymentLink()) - .executeAsync(); - break; - case R.id.share_link_item: - ShareUtils.shareFeedItemLink(context, selectedItem); - break; - default: - return false; - } - // Refresh menu state + public static boolean onMenuItemClicked(Context context, int menuItemId, + FeedItem selectedItem) throws DownloadRequestException { + DownloadRequester requester = DownloadRequester.getInstance(); + switch (menuItemId) { + case R.id.skip_episode_item: + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + case R.id.download_item: + DBTasks.downloadFeedItems(context, selectedItem); + break; + case R.id.play_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + false); + break; + case R.id.remove_item: + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); + break; + case R.id.cancel_download_item: + requester.cancelDownload(context, selectedItem.getMedia()); + break; + case R.id.mark_read_item: + DBWriter.markItemRead(context, selectedItem, true, true); + break; + case R.id.mark_unread_item: + DBWriter.markItemRead(context, selectedItem, false, true); + break; + case R.id.add_to_queue_item: + DBWriter.addQueueItem(context, selectedItem.getId()); + break; + case R.id.remove_from_queue_item: + DBWriter.removeQueueItem(context, selectedItem.getId(), true); + break; + case R.id.stream_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + true); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedItem.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrItemIfLoggedIn(context, selectedItem); + break; + case R.id.share_link_item: + ShareUtils.shareFeedItemLink(context, selectedItem); + break; + default: + return false; + } + // Refresh menu state - return true; - } + return true; + } } diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java index 27b1a8a8c..537335618 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java @@ -8,6 +8,10 @@ import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FeedInfoActivity; @@ -19,6 +23,7 @@ import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.ShareUtils; +import de.danoeh.antennapod.util.flattr.FlattrStatus; /** Handles interactions with the FeedItemMenu. */ public class FeedMenuHandler { @@ -38,9 +43,10 @@ public class FeedMenuHandler { Log.d(TAG, "Preparing options menu"); menu.findItem(R.id.mark_all_read_item).setVisible( selectedFeed.hasNewItems(true)); - if (selectedFeed.getPaymentLink() != null) { + if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) menu.findItem(R.id.support_item).setVisible(true); - } + else + menu.findItem(R.id.support_item).setVisible(false); MenuItem refresh = menu.findItem(R.id.refresh_item); if (DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFile( @@ -78,8 +84,7 @@ public class FeedMenuHandler { context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); break; case R.id.support_item: - new FlattrClickWorker(context, selectedFeed.getPaymentLink()) - .executeAsync(); + DBTasks.flattrFeedIfLoggedIn(context, selectedFeed); break; case R.id.share_link_item: ShareUtils.shareFeedlink(context, selectedFeed); diff --git a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java index 68d31324d..0945303e4 100644 --- a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java +++ b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java @@ -27,4 +27,9 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { throw new UnsupportedOperationException("Setting display not supported in Audio Player"); } } + + @Override + public void setVideoScalingMode(int mode) { + throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); + } } diff --git a/src/de/danoeh/antennapod/util/playback/IPlayer.java b/src/de/danoeh/antennapod/util/playback/IPlayer.java index ca9b36358..8c1cf4ef4 100644 --- a/src/de/danoeh/antennapod/util/playback/IPlayer.java +++ b/src/de/danoeh/antennapod/util/playback/IPlayer.java @@ -61,4 +61,6 @@ public interface IPlayer { void start(); void stop(); + + public void setVideoScalingMode(int mode); } diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java index 017a0cd5b..0781800aa 100644 --- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java +++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java @@ -1,25 +1,14 @@ package de.danoeh.antennapod.util.playback; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.SharedPreferences; +import android.content.*; import android.content.res.TypedArray; +import android.media.MediaPlayer; import android.os.AsyncTask; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; +import android.util.Pair; import android.view.SurfaceHolder; import android.view.View; import android.view.View.OnClickListener; @@ -30,13 +19,17 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.feed.MediaType; import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.service.PlaybackService; -import de.danoeh.antennapod.service.PlayerStatus; +import de.danoeh.antennapod.service.playback.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.service.playback.PlayerStatus; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.playback.Playable.PlayableUtils; +import java.util.concurrent.*; + /** * Communicates with the playback service. GUI classes should use this class to * control playback instead of communicating with the PlaybackService directly. @@ -44,10 +37,10 @@ import de.danoeh.antennapod.util.playback.Playable.PlayableUtils; public abstract class PlaybackController { private static final String TAG = "PlaybackController"; - public static final int DEFAULT_SEEK_DELTA = 30000; - public static final int INVALID_TIME = -1; + public static final int DEFAULT_SEEK_DELTA = 30000; + public static final int INVALID_TIME = -1; - private Activity activity; + private final Activity activity; private PlaybackService playbackService; private Playable media; @@ -69,6 +62,8 @@ public abstract class PlaybackController { private boolean reinitOnPause; public PlaybackController(Activity activity, boolean reinitOnPause) { + if (activity == null) + throw new IllegalArgumentException("activity = null"); this.activity = activity; this.reinitOnPause = reinitOnPause; schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, @@ -157,9 +152,6 @@ public abstract class PlaybackController { */ public void pause() { mediaInfoLoaded = false; - if (playbackService != null && playbackService.isPlayingVideo()) { - playbackService.pause(true, true); - } } /** @@ -179,8 +171,9 @@ public abstract class PlaybackController { @Override protected void onPostExecute(Intent serviceIntent) { boolean bound = false; - if (!PlaybackService.isRunning) { + if (!PlaybackService.started) { if (serviceIntent != null) { + if (AppConfig.DEBUG) Log.d(TAG, "Calling start service"); activity.startService(serviceIntent); bound = activity.bindService(serviceIntent, mConnection, 0); } else { @@ -297,7 +290,9 @@ public abstract class PlaybackController { if (AppConfig.DEBUG) Log.d(TAG, "Received statusUpdate Intent."); if (isConnectedToPlaybackService()) { - status = playbackService.getStatus(); + PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); + status = info.playerStatus; + media = info.playable; handleStatus(); } else { Log.w(TAG, @@ -328,10 +323,9 @@ public abstract class PlaybackController { case PlaybackService.NOTIFICATION_TYPE_RELOAD: cancelPositionObserver(); mediaInfoLoaded = false; + queryService(); onReloadNotification(intent.getIntExtra( PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); - queryService(); - break; case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: onSleepTimerUpdate(); @@ -401,37 +395,51 @@ public abstract class PlaybackController { * should be used to update the GUI or start/cancel background threads. */ private void handleStatus() { - TypedArray res = activity.obtainStyledAttributes(new int[]{ - R.attr.av_play, R.attr.av_pause}); - final int playResource = res.getResourceId(0, R.drawable.av_play); - final int pauseResource = res.getResourceId(1, R.drawable.av_pause); - res.recycle(); + final int playResource; + final int pauseResource; + final CharSequence playText = activity.getString(R.string.play_label); + final CharSequence pauseText = activity.getString(R.string.pause_label); + + if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) { + TypedArray res = activity.obtainStyledAttributes(new int[]{ + R.attr.av_play, R.attr.av_pause}); + playResource = res.getResourceId(0, R.drawable.av_play); + pauseResource = res.getResourceId(1, R.drawable.av_pause); + res.recycle(); + } else { + playResource = R.drawable.ic_action_play_over_video; + pauseResource = R.drawable.ic_action_pause_over_video; + } switch (status) { case ERROR: postStatusMsg(R.string.player_error_msg); + handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); break; case PAUSED: clearStatusMsg(); checkMediaInfoLoaded(); cancelPositionObserver(); - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); break; case PLAYING: clearStatusMsg(); checkMediaInfoLoaded(); + if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { + onAwaitingVideoSurface(); + } setupPositionObserver(); - updatePlayButtonAppearance(pauseResource); + updatePlayButtonAppearance(pauseResource, pauseText); break; case PREPARING: postStatusMsg(R.string.player_preparing_msg); checkMediaInfoLoaded(); if (playbackService != null) { if (playbackService.isStartWhenPrepared()) { - updatePlayButtonAppearance(pauseResource); + updatePlayButtonAppearance(pauseResource, pauseText); } else { - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); } } break; @@ -441,32 +449,27 @@ public abstract class PlaybackController { case PREPARED: checkMediaInfoLoaded(); postStatusMsg(R.string.player_ready_msg); - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); break; case SEEKING: postStatusMsg(R.string.player_seeking_msg); break; - case AWAITING_VIDEO_SURFACE: - onAwaitingVideoSurface(); - break; case INITIALIZED: checkMediaInfoLoaded(); clearStatusMsg(); - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); break; } } private void checkMediaInfoLoaded() { - if (!mediaInfoLoaded) { - loadMediaInfo(); - } - mediaInfoLoaded = true; + mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo()); } - private void updatePlayButtonAppearance(int resource) { + private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { ImageButton butPlay = getPlayButton(); butPlay.setImageResource(resource); + butPlay.setContentDescription(contentDescription); } public abstract ImageButton getPlayButton(); @@ -475,7 +478,7 @@ public abstract class PlaybackController { public abstract void clearStatusMsg(); - public abstract void loadMediaInfo(); + public abstract boolean loadMediaInfo(); public abstract void onAwaitingVideoSurface(); @@ -488,7 +491,8 @@ public abstract class PlaybackController { Log.d(TAG, "Querying service info"); if (playbackService != null) { status = playbackService.getStatus(); - media = playbackService.getMedia(); + media = playbackService.getPlayable(); + /* if (media == null) { Log.w(TAG, "PlaybackService has no media object. Trying to restore last played media."); @@ -497,6 +501,7 @@ public abstract class PlaybackController { activity.startService(serviceIntent); } } + */ onServiceQueried(); setupGUI(); @@ -517,7 +522,7 @@ public abstract class PlaybackController { */ public float onSeekBarProgressChanged(SeekBar seekBar, int progress, boolean fromUser, TextView txtvPosition) { - if (fromUser && playbackService != null) { + if (fromUser && playbackService != null && media != null) { float prog = progress / ((float) seekBar.getMax()); int duration = media.getDuration(); txtvPosition.setText(Converter @@ -541,7 +546,7 @@ public abstract class PlaybackController { */ public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { if (playbackService != null) { - playbackService.seek((int) (prog * media.getDuration())); + playbackService.seekTo((int) (prog * media.getDuration())); setupPositionObserver(); } } @@ -557,7 +562,7 @@ public abstract class PlaybackController { break; case PAUSED: case PREPARED: - playbackService.play(); + playbackService.resume(); break; case PREPARING: playbackService.setStartWhenPrepared(!playbackService @@ -609,7 +614,7 @@ public abstract class PlaybackController { public int getPosition() { if (playbackService != null) { - return playbackService.getCurrentPositionSafe(); + return playbackService.getCurrentPosition(); } else { return PlaybackService.INVALID_TIME; } @@ -617,7 +622,7 @@ public abstract class PlaybackController { public int getDuration() { if (playbackService != null) { - return playbackService.getDurationSafe(); + return playbackService.getDuration(); } else { return PlaybackService.INVALID_TIME; } @@ -675,27 +680,35 @@ public abstract class PlaybackController { return playbackService != null && playbackService.canSetSpeed(); } - public void setPlaybackSpeed(float speed) { - if (playbackService != null) { - playbackService.setSpeed(speed); - } - } - - public float getCurrentPlaybackSpeedMultiplier() { - if (canSetPlaybackSpeed()) { - return playbackService.getCurrentPlaybackSpeed(); - } else { - return -1; - } - } + public void setPlaybackSpeed(float speed) { + if (playbackService != null) { + playbackService.setSpeed(speed); + } + } + + public float getCurrentPlaybackSpeedMultiplier() { + if (canSetPlaybackSpeed()) { + return playbackService.getCurrentPlaybackSpeed(); + } else { + return -1; + } + } public boolean isPlayingVideo() { if (playbackService != null) { - return PlaybackService.isPlayingVideo(); + return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; } return false; } + public Pair getVideoSize() { + if (playbackService != null) { + return playbackService.getVideoSize(); + } else { + return null; + } + } + /** * Returns true if PlaybackController can communicate with the playback @@ -716,7 +729,7 @@ public abstract class PlaybackController { */ public void reinitServiceIfPaused() { if (playbackService != null - && playbackService.isShouldStream() + && playbackService.isStreaming() && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService .getStatus() == PlayerStatus.PREPARING && playbackService .isStartWhenPrepared() == false))) { @@ -733,8 +746,7 @@ public abstract class PlaybackController { @Override public void run() { - if (playbackService != null && playbackService.getPlayer() != null - && playbackService.getPlayer().isPlaying()) { + if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) { activity.runOnUiThread(new Runnable() { @Override diff --git a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java index f0a50542c..ea9c692ab 100644 --- a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java +++ b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java @@ -59,4 +59,9 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { Log.e(TAG, "Setting playback speed unsupported in video player"); throw new UnsupportedOperationException("Setting playback speed unsupported in video player"); } + + @Override + public void setVideoScalingMode(int mode) { + super.setVideoScalingMode(mode); + } } diff --git a/src/de/danoeh/antennapod/view/AspectRatioVideoView.java b/src/de/danoeh/antennapod/view/AspectRatioVideoView.java new file mode 100644 index 000000000..f930c912a --- /dev/null +++ b/src/de/danoeh/antennapod/view/AspectRatioVideoView.java @@ -0,0 +1,97 @@ +package de.danoeh.antennapod.view; + +/* + * Copyright (C) Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.VideoView; + +public class AspectRatioVideoView extends VideoView { + + + private int mVideoWidth; + private int mVideoHeight; + + public AspectRatioVideoView(Context context) { + this(context, null); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mVideoWidth = 0; + mVideoHeight = 0; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mVideoWidth <= 0 || mVideoHeight <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + float heightRatio = (float) mVideoHeight / (float) getHeight(); + float widthRatio = (float) mVideoWidth / (float) getWidth(); + + int scaledHeight; + int scaledWidth; + + if (heightRatio > widthRatio) { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / heightRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / heightRatio); + } else { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / widthRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / widthRatio); + } + + setMeasuredDimension(scaledWidth, scaledHeight); + } + + /** + * Source code originally from: + * http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625 + * + * @param videoWidth + * @param videoHeight + */ + public void setVideoSize(int videoWidth, int videoHeight) { + // Set the new video size + mVideoWidth = videoWidth; + mVideoHeight = videoHeight; + + /** + * If this isn't set the video is stretched across the + * SurfaceHolders display surface (i.e. the SurfaceHolder + * as the same size and the video is drawn to fit this + * display area). We want the size to be the video size + * and allow the aspectratio to handle how the surface is shown + */ + getHolder().setFixedSize(videoWidth, videoHeight); + + requestLayout(); + invalidate(); + } + +} diff --git a/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java b/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java index 8df35ce67..5506a3bc9 100644 --- a/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java +++ b/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java @@ -108,6 +108,12 @@ public class HttpDownloaderTest extends InstrumentationTestCase { assertFalse(new File(feedFile.getFile_url()).exists()); } + /* TODO: replace with smaller test file + public void testUrlWithSpaces() { + download("http://acedl.noxsolutions.com/ace/Don't Call Salman Rushdie Sneezy in Finland.mp3", "testUrlWithSpaces", true); + } + */ + private static class FeedFileImpl extends FeedFile { public FeedFileImpl(String download_url) { super(null, download_url, false); diff --git a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java new file mode 100644 index 000000000..8a270715f --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java @@ -0,0 +1,1169 @@ +package instrumentationTest.de.test.antennapod.service.playback; + +import android.content.Context; +import android.media.RemoteControlClient; +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.service.playback.PlayerStatus; +import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.playback.Playable; +import junit.framework.AssertionFailedError; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test class for PlaybackServiceMediaPlayer + */ +public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { + private static final String TAG = "PlaybackServiceMediaPlayerTest"; + + private static final String PLAYABLE_FILE_URL = "http://hpr.dogphilosophy.net/test/mp3.mp3"; + private static final String PLAYABLE_DEST_URL = "psmptestfile.wav"; + private String PLAYABLE_LOCAL_URL = null; + private static final int LATCH_TIMEOUT_SECONDS = 10; + + private volatile AssertionFailedError assertionError; + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + assertionError = null; + + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + File cacheDir = context.getExternalFilesDir("testFiles"); + if (cacheDir == null) + cacheDir = context.getExternalFilesDir("testFiles"); + File dest = new File(cacheDir, PLAYABLE_DEST_URL); + + assertNotNull(cacheDir); + assertTrue(cacheDir.canWrite()); + assertTrue(cacheDir.canRead()); + if (!dest.exists()) { + InputStream i = new URL(PLAYABLE_FILE_URL).openStream(); + OutputStream o = new FileOutputStream(new File(cacheDir, PLAYABLE_DEST_URL)); + IOUtils.copy(i, o); + o.flush(); + o.close(); + i.close(); + } + PLAYABLE_LOCAL_URL = "file://" + dest.getAbsolutePath(); + } + + private void checkPSMPInfo(PlaybackServiceMediaPlayer.PSMPInfo info) { + try { + switch (info.playerStatus) { + case PLAYING: + case PAUSED: + case PREPARED: + case PREPARING: + case INITIALIZED: + case INITIALIZING: + case SEEKING: + assertNotNull(info.playable); + break; + case STOPPED: + assertNull(info.playable); + break; + case ERROR: + assertNull(info.playable); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + public void testInit() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, defaultCallback); + psmp.shutdown(); + } + + private Playable writeTestPlayable(String downloadUrl, String fileUrl) { + final Context c = getInstrumentation().getTargetContext(); + Feed f = new Feed(0, new Date(), "f", "l", "d", null, null, null, null, "i", null, null, "l", false); + f.setItems(new ArrayList()); + FeedItem i = new FeedItem(0, "t", "i", "l", new Date(), false, f); + f.getItems().add(i); + FeedMedia media = new FeedMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0); + i.setMedia(media); + PodDBAdapter adapter = new PodDBAdapter(c); + adapter.open(); + adapter.setCompleteFeed(f); + assertTrue(media.getId() != 0); + adapter.close(); + return media; + } + + + public void testPlayMediaObjectStreamNoStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, false, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertFalse(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, true, false); + + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertTrue(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamNoStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(4); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, false, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED); + + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(5); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + + } else if (countDownLatch.getCount() == 5) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, true, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalNoStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, false, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertFalse(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, true, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertTrue(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalNoStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(4); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, false, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(5); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 5) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus); + } + + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } finally { + countDownLatch.countDown(); + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, true, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING); + psmp.shutdown(); + } + + + private final PlaybackServiceMediaPlayer.PSMPCallback defaultCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + + private void pauseTestSkeleton(final PlayerStatus initialState, final boolean stream, final boolean abandonAudioFocus, final boolean reinit, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = (stream && reinit) ? 2 : 1; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else if (initialState != PlayerStatus.PLAYING) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + switch (newInfo.playerStatus) { + case PAUSED: + if (latchCount == countDownLatch.getCount()) + countDownLatch.countDown(); + else { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } + break; + case INITIALIZED: + if (stream && reinit && countDownLatch.getCount() < latchCount) { + countDownLatch.countDown(); + } else if (countDownLatch.getCount() < latchCount) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } + break; + } + } + + } + + @Override + public void shouldStop() { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to shouldStop"); + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + if (initialState == PlayerStatus.PLAYING) { + psmp.playMediaObject(p, stream, true, true); + } + psmp.pause(abandonAudioFocus, reinit); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res || initialState != PlayerStatus.PLAYING); + psmp.shutdown(); + } + + public void testPauseDefaultState() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.STOPPED, false, false, false, 1); + } + + public void testPausePlayingStateNoAbandonNoReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, false, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonNoReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, false, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonNoReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, true, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonNoReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, true, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, false, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, false, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, true, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, true, true, LATCH_TIMEOUT_SECONDS); + } + + private void resumeTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = (initialState == PlayerStatus.PAUSED || initialState == PlayerStatus.PLAYING) ? 2 : + (initialState == PlayerStatus.PREPARED) ? 1 : 0; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else if (newInfo.playerStatus == PlayerStatus.PLAYING) { + if (countDownLatch.getCount() == 0) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + countDownLatch.countDown(); + } + } + + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) { + assertionError = new AssertionFailedError("Unexpected call of onMediaPlayerError"); + } + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) { + boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED); + psmp.playMediaObject(writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL), false, startWhenPrepared, true); + } + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.resume(); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res || (initialState != PlayerStatus.PAUSED && initialState != PlayerStatus.PREPARED)); + psmp.shutdown(); + } + + public void testResumePausedState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS); + } + + public void testResumePreparedState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS); + } + + public void testResumePlayingState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PLAYING, 1); + } + + private void prepareTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = 1; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + if (initialState == PlayerStatus.INITIALIZED && newInfo.playerStatus == PlayerStatus.PREPARED) { + countDownLatch.countDown(); + } else if (initialState != PlayerStatus.INITIALIZED && initialState == newInfo.playerStatus) { + countDownLatch.countDown(); + } + } + + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + if (initialState == PlayerStatus.INITIALIZED + || initialState == PlayerStatus.PLAYING + || initialState == PlayerStatus.PREPARED + || initialState == PlayerStatus.PAUSED) { + boolean prepareImmediately = (initialState != PlayerStatus.INITIALIZED); + boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED); + psmp.playMediaObject(p, false, startWhenPrepared, prepareImmediately); + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.prepare(); + } + + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (initialState != PlayerStatus.INITIALIZED) { + assertEquals(initialState, psmp.getPSMPInfo().playerStatus); + } + + if (assertionError != null) + throw assertionError; + assertTrue(res); + psmp.shutdown(); + } + + public void testPrepareInitializedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS); + } + + public void testPreparePlayingState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PLAYING, 1); + } + + public void testPreparePausedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PAUSED, 1); + } + + public void testPreparePreparedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PREPARED, 1); + } + + private void reinitTestSkeleton(final PlayerStatus initialState, final long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = 2; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + if (newInfo.playerStatus == initialState) { + countDownLatch.countDown(); + } else if (countDownLatch.getCount() < latchCount && newInfo.playerStatus == PlayerStatus.INITIALIZED) { + countDownLatch.countDown(); + } + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + boolean prepareImmediately = initialState != PlayerStatus.INITIALIZED; + boolean startImmediately = initialState != PlayerStatus.PREPARED; + psmp.playMediaObject(p, false, startImmediately, prepareImmediately); + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.reinit(); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + psmp.shutdown(); + } + + public void testReinitPlayingState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PLAYING, LATCH_TIMEOUT_SECONDS); + } + + public void testReinitPausedState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS); + } + + public void testPreparedPlayingState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS); + } + + public void testReinitInitializedState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS); + } + + private static class UnexpectedStateChange extends AssertionFailedError { + public UnexpectedStateChange(PlayerStatus status) { + super("Unexpected state change: " + status); + } + } +} diff --git a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java new file mode 100644 index 000000000..19f64b4cf --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java @@ -0,0 +1,333 @@ +package instrumentationTest.de.test.antennapod.service.playback; + +import android.content.Context; +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.feed.EventDistributor; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.service.playback.PlaybackServiceTaskManager; +import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.playback.Playable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test class for PlaybackServiceTaskManager + */ +public class PlaybackServiceTaskManagerTest extends InstrumentationTestCase { + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + assertTrue(PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext())); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + } + + public void testInit() { + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(getInstrumentation().getTargetContext(), defaultPSTM); + pstm.shutdown(); + } + + private List writeTestQueue(String pref) { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + Feed f = new Feed(0, new Date(), "title", "link", "d", null, null, null, null, "id", null, "null", "url", false); + f.setItems(new ArrayList()); + for (int i = 0; i < NUM_ITEMS; i++) { + f.getItems().add(new FeedItem(0, pref + i, pref + i, "link", new Date(), true, f)); + } + PodDBAdapter adapter = new PodDBAdapter(c); + adapter.open(); + adapter.setCompleteFeed(f); + adapter.setQueue(f.getItems()); + adapter.close(); + + for (FeedItem item : f.getItems()) { + assertTrue(item.getId() != 0); + } + return f.getItems(); + } + + public void testGetQueueWriteBeforeCreation() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + List queue = writeTestQueue("a"); + assertNotNull(queue); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + List testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(queue.size() == testQueue.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(queue.get(i).getId() == testQueue.get(i).getId()); + } + pstm.shutdown(); + } + + public void testGetQueueWriteAfterCreation() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + List testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(testQueue.isEmpty()); + + + final CountDownLatch countDownLatch = new CountDownLatch(1); + EventDistributor.EventListener queueListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + countDownLatch.countDown(); + } + }; + EventDistributor.getInstance().register(queueListener); + List queue = writeTestQueue("a"); + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + countDownLatch.await(5000, TimeUnit.MILLISECONDS); + + assertNotNull(queue); + testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(queue.size() == testQueue.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(queue.get(i).getId() == testQueue.get(i).getId()); + } + pstm.shutdown(); + } + + public void testStartPositionSaver() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_COUNTDOWNS = 2; + final int TIMEOUT = 3 * PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL; + final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + countDownLatch.countDown(); + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.startPositionSaver(); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testIsPositionSaverActive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startPositionSaver(); + assertTrue(pstm.isPositionSaverActive()); + pstm.shutdown(); + } + + public void testCancelPositionSaver() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startPositionSaver(); + pstm.cancelPositionSaver(); + assertFalse(pstm.isPositionSaverActive()); + pstm.shutdown(); + } + + public void testStartWidgetUpdater() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_COUNTDOWNS = 2; + final int TIMEOUT = 3 * PlaybackServiceTaskManager.WIDGET_UPDATER_NOTIFICATION_INTERVAL; + final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + countDownLatch.countDown(); + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.startWidgetUpdater(); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testIsWidgetUpdaterActive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + assertTrue(pstm.isWidgetUpdaterActive()); + pstm.shutdown(); + } + + public void testCancelWidgetUpdater() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + pstm.cancelWidgetUpdater(); + assertFalse(pstm.isWidgetUpdaterActive()); + pstm.shutdown(); + } + + public void testCancelAllTasksNoTasksStarted() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.cancelAllTasks(); + assertFalse(pstm.isPositionSaverActive()); + assertFalse(pstm.isWidgetUpdaterActive()); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testCancelAllTasksAllTasksStarted() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + pstm.startPositionSaver(); + pstm.setSleepTimer(100000); + pstm.cancelAllTasks(); + assertFalse(pstm.isPositionSaverActive()); + assertFalse(pstm.isWidgetUpdaterActive()); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testSetSleepTimer() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final long TIME = 2000; + final long TIMEOUT = 2 * TIME; + final CountDownLatch countDownLatch = new CountDownLatch(1); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + if (countDownLatch.getCount() == 0) { + fail(); + } + countDownLatch.countDown(); + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.setSleepTimer(TIME); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testDisableSleepTimer() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final long TIME = 1000; + final long TIMEOUT = 2 * TIME; + final CountDownLatch countDownLatch = new CountDownLatch(1); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + fail("Sleeptimer expired"); + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.setSleepTimer(TIME); + pstm.disableSleepTimer(); + assertFalse(countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)); + pstm.shutdown(); + } + + public void testIsSleepTimerActivePositive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.setSleepTimer(10000); + assertTrue(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testIsSleepTimerActiveNegative() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.setSleepTimer(10000); + pstm.disableSleepTimer(); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + private final PlaybackServiceTaskManager.PSTMCallback defaultPSTM = new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }; +} diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java index 91ac61867..b03d83d25 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java @@ -4,11 +4,19 @@ import android.content.Context; import android.test.InstrumentationTestCase; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.FeedItemStatistics; import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import static instrumentationTest.de.test.antennapod.storage.DBTestUtils.*; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + +import static instrumentationTest.de.test.antennapod.storage.DBTestUtils.saveFeedlist; /** * Test class for DBReader @@ -36,7 +44,7 @@ public class DBReaderTest extends InstrumentationTestCase { private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) { final Context context = getInstrumentation().getTargetContext(); Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, - null, null, null, "feed", null, null, "url", false); + null, null, null, "feed", null, null, "url", false, new FlattrStatus()); feed.setItems(new ArrayList()); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); @@ -65,7 +73,6 @@ public class DBReaderTest extends InstrumentationTestCase { } - public void testGetFeedList() { final Context context = getInstrumentation().getTargetContext(); List feeds = saveFeedlist(context, 10, 0, false); @@ -77,6 +84,36 @@ public class DBReaderTest extends InstrumentationTestCase { } } + public void testGetFeedListSortOrder() { + final Context context = getInstrumentation().getTargetContext(); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Feed feed1 = new Feed(0, new Date(), "A", "link", "d", null, null, null, "rss", "A", null, "", "", true); + Feed feed2 = new Feed(0, new Date(), "b", "link", "d", null, null, null, "rss", "b", null, "", "", true); + Feed feed3 = new Feed(0, new Date(), "C", "link", "d", null, null, null, "rss", "C", null, "", "", true); + Feed feed4 = new Feed(0, new Date(), "d", "link", "d", null, null, null, "rss", "d", null, "", "", true); + adapter.setCompleteFeed(feed1); + adapter.setCompleteFeed(feed2); + adapter.setCompleteFeed(feed3); + adapter.setCompleteFeed(feed4); + assertTrue(feed1.getId() != 0); + assertTrue(feed2.getId() != 0); + assertTrue(feed3.getId() != 0); + assertTrue(feed4.getId() != 0); + + adapter.close(); + + List saved = DBReader.getFeedList(context); + assertNotNull(saved); + assertEquals("Wrong size: ", 4, saved.size()); + + assertEquals("Wrong id of feed 1: ", feed1.getId(), saved.get(0).getId()); + assertEquals("Wrong id of feed 2: ", feed2.getId(), saved.get(1).getId()); + assertEquals("Wrong id of feed 3: ", feed3.getId(), saved.get(2).getId()); + assertEquals("Wrong id of feed 4: ", feed4.getId(), saved.get(3).getId()); + } + public void testFeedListDownloadUrls() { final Context context = getInstrumentation().getTargetContext(); List feeds = saveFeedlist(context, 10, 0, false); @@ -286,4 +323,46 @@ public class DBReaderTest extends InstrumentationTestCase { assertTrue(found); } } + + public void testGetPlaybackHistory() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + final int playedItems = 5; + final int numFeeds = 1; + + Feed feed = DBTestUtils.saveFeedlist(context, numFeeds, numItems, true).get(0); + long[] ids = new long[playedItems]; + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (int i = 0; i < playedItems; i++) { + FeedMedia m = feed.getItems().get(i).getMedia(); + m.setPlaybackCompletionDate(new Date(i + 1)); + adapter.setFeedMediaPlaybackCompletionDate(m); + ids[ids.length - 1 - i] = m.getItem().getId(); + } + adapter.close(); + + List saved = DBReader.getPlaybackHistory(context); + assertNotNull(saved); + assertEquals("Wrong size: ", playedItems, saved.size()); + for (int i = 0; i < playedItems; i++) { + FeedItem item = saved.get(i); + assertNotNull(item.getMedia().getPlaybackCompletionDate()); + assertEquals("Wrong sort order: ", item.getId(), ids[i]); + } + } + + public void testGetFeedStatisticsCheckOrder() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_FEEDS = 10; + final int NUM_ITEMS = 10; + List feeds = DBTestUtils.saveFeedlist(context, NUM_FEEDS, NUM_ITEMS, false); + List statistics = DBReader.getFeedStatisticsList(context); + assertNotNull(statistics); + assertEquals(feeds.size(), statistics.size()); + for (int i = 0; i < NUM_FEEDS; i++) { + assertEquals("Wrong entry at index " + i, feeds.get(i).getId(), statistics.get(i).getFeedID()); + } + } } diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java index e9b871867..2372757ce 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java @@ -11,6 +11,7 @@ import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import java.io.File; import java.io.IOException; @@ -76,7 +77,7 @@ public class DBTasksTest extends InstrumentationTestCase { File f = new File(destFolder, "file " + i); assertTrue(f.createNewFile()); files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i))); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); items.add(item); } @@ -114,7 +115,7 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(f.createNewFile()); assertTrue(f.exists()); files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i))); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); items.add(item); } @@ -148,7 +149,7 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(f.createNewFile()); assertTrue(f.exists()); files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i))); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); items.add(item); } @@ -279,7 +280,7 @@ public class DBTasksTest extends InstrumentationTestCase { final Context context = getInstrumentation().getTargetContext(); UserPreferences.setUpdateInterval(context, expirationTime); Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, - null, null, null, "feed", null, null, "url", false); + null, null, null, "feed", null, null, "url", false, new FlattrStatus()); feed.setItems(new ArrayList()); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java b/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java index fbb7b0386..7e9e1b908 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java @@ -6,6 +6,7 @@ import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.PodDBAdapter; import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import junit.framework.Assert; import java.util.ArrayList; @@ -31,7 +32,7 @@ public class DBTestUtils { adapter.open(); for (int i = 0; i < numFeeds; i++) { Feed f = new Feed(0, new Date(), "feed " + i, "link" + i, "descr", null, null, - null, null, "id" + i, null, null, "url" + i, false); + null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus()); f.setItems(new ArrayList()); for (int j = 0; j < numItems; j++) { FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(), diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java index 429903cba..679ae1ad3 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java @@ -64,7 +64,7 @@ public class DBWriterTest extends InstrumentationTestCase { feed.setItems(items); FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), true, feed); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null, 0); item.setMedia(media); items.add(item); @@ -108,7 +108,7 @@ public class DBWriterTest extends InstrumentationTestCase { assertTrue(enc.createNewFile()); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0); item.setMedia(media); } @@ -169,7 +169,7 @@ public class DBWriterTest extends InstrumentationTestCase { assertTrue(enc.createNewFile()); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0); item.setMedia(media); } @@ -317,7 +317,7 @@ public class DBWriterTest extends InstrumentationTestCase { File enc = new File(destFolder, "file " + i); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0); item.setMedia(media); } @@ -389,7 +389,7 @@ public class DBWriterTest extends InstrumentationTestCase { File enc = new File(destFolder, "file " + i); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0); item.setMedia(media); } @@ -430,7 +430,7 @@ public class DBWriterTest extends InstrumentationTestCase { Feed feed = new Feed("url", new Date(), "title"); feed.setItems(new ArrayList()); FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed); - FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, "url", false, playbackCompletionDate); + FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, "url", false, playbackCompletionDate, 0); feed.getItems().add(item); item.setMedia(media); PodDBAdapter adapter = new PodDBAdapter(context); diff --git a/submodules/dslv b/submodules/dslv index f294d8eec..1bb29afb9 160000 --- a/submodules/dslv +++ b/submodules/dslv @@ -1 +1 @@ -Subproject commit f294d8eec59f73b5594634cac6fe1dc4e2cb32b6 +Subproject commit 1bb29afb91f41af53f136599cf349752c13f52ab