diff options
73 files changed, 2554 insertions, 2087 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 65aa8ea59..b9c5a6d3e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.danoeh.antennapod" - android:versionCode="39" - android:versionName="0.9.9.2"> + android:versionCode="41" + android:versionName="0.9.9.4"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cdd715ae..0c7b8a701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ Change Log ========== +Version 0.9.9.4 +--------------- +* Added support for MP4 chapters (currently only for arm devices and downloaded episodes) +* Fixed a bug where episode images were not loaded correctly +* Fixed battery usage problems + +Version 0.9.9.3 +--------------- +* Fixed video playback problems +* Improved image loading +* Other bugfixes and improvements + Version 0.9.9.2 --------------- * Added support for feed discovery if a website URL is entered @@ -2,11 +2,8 @@ This is the official repository of AntennaPod, a podcast manager for Android. - -<a href="https://play.google.com/store/apps/details?id=de.danoeh.antennapod" alt="Download from Google Play"> - <img src="http://www.android.com/images/brand/android_app_on_play_large.png"> -</a> -[AntennaPod on fdroid.org](http://f-droid.org/repository/browse/?fdcategory=Multimedia&fdid=de.danoeh.antennapod&fdpage=1) +[![Download from Google Play](http://www.android.com/images/brand/android_app_on_play_large.png "Download from Google Play")](https://play.google.com/store/apps/details?id=de.danoeh.antennapod) +[![AntennaPod on fdroid.org](https://camo.githubusercontent.com/7df0eafa4433fa4919a56f87c3d99cf81b68d01c/68747470733a2f2f662d64726f69642e6f72672f77696b692f696d616765732f632f63342f462d44726f69642d627574746f6e5f617661696c61626c652d6f6e2e706e67 "Download from fdroid.org")](http://f-droid.org/repository/browse/?fdcategory=Multimedia&fdid=de.danoeh.antennapod&fdpage=1) ## Feedback You can use the [AntennaPod Google Group](https://groups.google.com/forum/#!forum/antennapod) for discussions about the app. @@ -36,5 +33,7 @@ Information on how to build AntennaPod can be found in the [Wiki](https://github [![Flattr Button](http://api.flattr.com/button/button-static-50x60.png "Flattr This!")](https://flattr.com/thing/745609/Antennapod "AntennaPod") -Bitcoin donations can be sent to this address: <pre>1DzvtuvdW8VhDsq9GUytMyALmsHeaHEKbg</pre> +![Donate Bitcoin](https://en.bitcoin.it/w/images/en/7/74/BC_Rnd_64px.png) +Bitcoin address: `1DzvtuvdW8VhDsq9GUytMyALmsHeaHEKbg` +[![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=370084)](https://www.bountysource.com/trackers/370084-antennapod?utm_source=370084&utm_medium=shield&utm_campaign=TRACKER_BADGE) diff --git a/assets/LICENSE_FFMPEGMEDIAMETADATARETRIEVER.txt b/assets/LICENSE_FFMPEGMEDIAMETADATARETRIEVER.txt new file mode 100644 index 000000000..fcaf80557 --- /dev/null +++ b/assets/LICENSE_FFMPEGMEDIAMETADATARETRIEVER.txt @@ -0,0 +1,16 @@ +FFmpegMediaMetadataRetriever: A unified interface for retrieving frame +and meta data from an input media file. + +Copyright 2014 William Seemann + +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. diff --git a/assets/LICENSE_OKHTTP.txt b/assets/LICENSE_OKHTTP.txt new file mode 100644 index 000000000..90edcee40 --- /dev/null +++ b/assets/LICENSE_OKHTTP.txt @@ -0,0 +1,11 @@ +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. diff --git a/assets/LICENSE_OKIO.txt b/assets/LICENSE_OKIO.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/assets/LICENSE_OKIO.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/assets/LICENSE_PICASSO.txt b/assets/LICENSE_PICASSO.txt new file mode 100644 index 000000000..0bf6b9f8e --- /dev/null +++ b/assets/LICENSE_PICASSO.txt @@ -0,0 +1,13 @@ +Copyright 2013 Square, Inc. + +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. diff --git a/assets/about.html b/assets/about.html index d572b7518..58cb71c35 100644 --- a/assets/about.html +++ b/assets/about.html @@ -41,7 +41,7 @@ <div id="header" align="center"> <img src="logo.png" alt="Logo" width="100px" height="100px"/> - <p>AntennaPod, Version 0.9.9.2</p> + <p>AntennaPod, Version 0.9.9.4</p> <p>Copyright © 2014 Daniel Oeh</p> @@ -53,16 +53,38 @@ by Jake Wharton, licensed under the Apache 2.0 license <a href="LICENSE_NINE_OLD_ANDROIDS.txt">(View)</a> <h2>Apache Commons <a href="http://commons.apache.org/">(Link)</a></h2> -by The Apache Software Foundation, licensed under the Apache 2.0 license <a href="LICENSE_APACHE_COMMONS.txt">(View)</a> +by The Apache Software Foundation, licensed under the Apache 2.0 license <a + href="LICENSE_APACHE_COMMONS.txt">(View)</a> + <h2>flattr4j <a href="http://www.shredzone.org/projects/flattr4j/wiki">(Link)</a></h2> licensed under the Apache 2.0 license <a href="LICENSE_FLATTR4J.txt">(View)</a> + <h2>drag-sort-listview <a href="https://github.com/bauerca/drag-sort-listview">(Link)</a></h2> licensed under the Apache 2.0 license <a href="LICENSE_DSLV.txt">(View)</a> + <h2>Presto Client <a href="http://www.aocate.com/presto/">(Link)</a></h2> licensed under the Apache 2.0 license <a href="LICENSE_PRESTO.txt">(View)</a> + <h2>Better Pickers <a href="https://github.com/derekbrameyer/android-betterpickers">(Link)</a></h2> licensed under the Apache 2.0 license <a href="LICENSE_BETTERPICKERS.txt">(View)</a> + <h2>jsoup <a href="http://jsoup.org/">(Link)</a></h2> licensed under the MIT license <a href="LICENSE_JSOUP.txt">(View)</a> </body> +<h2>Picasso <a href="https://github.com/square/picasso">(Link)</a></h2> +licensed under the Apache 2.0 license <a href="LICENSE_PICASSO.txt">(View)</a> + +<h2>OkHttp <a href="https://github.com/square/okhttp">(Link)</a></h2> +licensed under the Apache 2.0 license <a href="LICENSE_OKHTTP.txt">(View)</a> + +<h2>Okio <a href="https://github.com/square/okio">(Link)</a></h2> +licensed under the Apache 2.0 license <a href="LICENSE_OKIO.txt">(View)</a> + +<h2>FFmpegMediaMetadataRetriever <a href="https://github.com/wseemann/FFmpegMediaMetadataRetriever">(Link)</a></h2> +licensed under the Apache 2.0 license <a href="LICENSE_FFMPEGMEDIAMETADATARETRIEVER.txt">(View)</a> + +<p>This software uses <a href="https://ffmpeg.org">FFmpeg</a> licensed under the <a href="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html"> + LGPLv2.1</a> and its source can be downloaded <a href="https://github.com/wseemann/FFmpegMediaMetadataRetriever/blob/master/fmmr-library/ffmpeg-2.1-android-2013-11-13.tar.gz">here</a>.</p> +</a> + </html> diff --git a/build.gradle b/build.gradle index 140d483da..7ce6664c4 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.12.+' + classpath 'com.android.tools.build:gradle:0.13.2' } } apply plugin: 'com.android.application' @@ -17,10 +17,10 @@ dependencies { println "Creating libs directory" libsdir.mkdir() } - compile 'com.android.support:support-v4:19.1.+' - compile 'com.android.support:appcompat-v7:19.1.+' + compile 'com.android.support:support-v4:20.0.0' + compile 'com.android.support:appcompat-v7:20.0.0' compile 'org.apache.commons:commons-lang3:3.3.2' - compile ('org.shredzone.flattr4j:flattr4j-core:2.10') { + compile ('org.shredzone.flattr4j:flattr4j-core:2.11') { exclude group: 'org.apache.httpcomponents', module: 'httpcore' exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.json', module: 'json' @@ -34,6 +34,10 @@ dependencies { exclude group: 'com.android.support', module: 'support-v4' } compile 'org.jsoup:jsoup:1.7.3' + compile 'com.squareup.picasso:picasso:2.3.4' + compile 'com.squareup.okhttp:okhttp:2.0.0' + compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0' + compile 'com.squareup.okio:okio:1.0.0' } android { @@ -81,17 +85,28 @@ android { renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['jniLibs'] } } buildTypes { + def STRING = "String" + def FLATTR_APP_KEY = "FLATTR_APP_KEY" + def FLATTR_APP_SECRET = "FLATTR_APP_SECRET" + def mFlattrAppKey = (project.hasProperty('flattrAppKey')) ? flattrAppKey : "\"\"" + def mFlattrAppSecret = (project.hasProperty('flattrAppSecret')) ? flattrAppSecret : "\"\"" + debug { applicationIdSuffix ".debug" + buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey + buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret } release { runProguard true proguardFile 'proguard.cfg' signingConfig signingConfigs.releaseConfig + buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey + buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret } } @@ -111,5 +126,5 @@ android { } task wrapper(type: Wrapper) { - gradleVersion = '1.12' + gradleVersion = '2.1' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differindex 3c7abdf12..3d0dee6e8 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b83aa49f2..6c3897a9c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun May 18 21:50:42 CEST 2014 +#Sun Sep 28 21:26:43 CEST 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip diff --git a/jniLibs/armeabi/libavcodec.so b/jniLibs/armeabi/libavcodec.so Binary files differnew file mode 100755 index 000000000..c6cd15096 --- /dev/null +++ b/jniLibs/armeabi/libavcodec.so diff --git a/jniLibs/armeabi/libavformat.so b/jniLibs/armeabi/libavformat.so Binary files differnew file mode 100755 index 000000000..b491bd5b2 --- /dev/null +++ b/jniLibs/armeabi/libavformat.so diff --git a/jniLibs/armeabi/libavutil.so b/jniLibs/armeabi/libavutil.so Binary files differnew file mode 100755 index 000000000..8451b8ba8 --- /dev/null +++ b/jniLibs/armeabi/libavutil.so diff --git a/jniLibs/armeabi/libcrypto.so b/jniLibs/armeabi/libcrypto.so Binary files differnew file mode 100644 index 000000000..b7de733d2 --- /dev/null +++ b/jniLibs/armeabi/libcrypto.so diff --git a/jniLibs/armeabi/libffmpeg_mediametadataretriever_jni.so b/jniLibs/armeabi/libffmpeg_mediametadataretriever_jni.so Binary files differnew file mode 100755 index 000000000..747191cc6 --- /dev/null +++ b/jniLibs/armeabi/libffmpeg_mediametadataretriever_jni.so diff --git a/jniLibs/armeabi/libssl.so b/jniLibs/armeabi/libssl.so Binary files differnew file mode 100644 index 000000000..623811e3a --- /dev/null +++ b/jniLibs/armeabi/libssl.so diff --git a/jniLibs/armeabi/libswscale.so b/jniLibs/armeabi/libswscale.so Binary files differnew file mode 100755 index 000000000..d0a3a18c1 --- /dev/null +++ b/jniLibs/armeabi/libswscale.so @@ -5,7 +5,7 @@ <groupId>de.danoeh</groupId> <artifactId>antennapod</artifactId> <packaging>apk</packaging> - <version>0.9.9.2</version> + <version>0.9.9.4</version> <name>AntennaPod</name> diff --git a/proguard.cfg b/proguard.cfg index 323e0b673..1838f007c 100644 --- a/proguard.cfg +++ b/proguard.cfg @@ -50,6 +50,10 @@ -keep public class org.jsoup.** { public *; } + +-dontwarn com.squareup.okhttp.** +-dontwarn okio.** + -keep class android.support.v4.** { *; } -keep interface android.support.v4.** { *; } -keep class android.support.v7.** { *; } diff --git a/res/layout/feeditem_dialog.xml b/res/layout/feeditem_dialog.xml index 7d05603e8..c8dca8460 100644 --- a/res/layout/feeditem_dialog.xml +++ b/res/layout/feeditem_dialog.xml @@ -11,7 +11,8 @@ android:layout_margin="16dp" android:id="@+id/txtvTitle" android:layout_alignParentTop="true" - style="@style/AntennaPod.Dialog.Title"/> + style="@style/AntennaPod.Dialog.Title" + android:maxLines="10"/> <View android:id="@+id/title_divider" diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index a9f96fb31..ae2addb05 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -31,6 +31,7 @@ <string name="copy_url_label">Copia l\'enllaç</string> <string name="share_url_label">Comparteix l\'enllaç</string> <string name="copied_url_msg">S\'ha copiat l\'enllaç al porta-retalls.</string> + <string name="go_to_position_label">Vés a aquesta posició</string> <!--Playback history--> <string name="clear_history_label">Esborra l\'historial</string> <!--Other--> @@ -59,6 +60,7 @@ <string name="auto_download_label">Inclou a baixades automà tiques</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Enllaç del canal</string> + <string name="etxtFeedurlHint">URL, canal o lloc web</string> <string name="txtvfeedurl_label">Afegeix podcast amb l\'URL</string> <string name="podcastdirectories_label">Cerca podcast al directori</string> <string name="podcastdirectories_descr">Podeu cercar nous podcasts al directori de gpodder.net mitjançant el seu nom, categoria o popularitat.</string> @@ -133,6 +135,7 @@ <string name="position_default_label">00:00:00</string> <string name="player_buffering_msg">S\'està carregant</string> <string name="playbackservice_notification_title">Podcast en reproducció</string> + <string name="unknown_media_key">AntennaPod - Control desconegut: %1$d</string> <!--Queue operations--> <string name="clear_queue_label">Buida la cua</string> <string name="undo">Desfés</string> @@ -146,6 +149,7 @@ <string name="return_home_label">Torna a l\'inici</string> <string name="flattr_auth_success">L\'autenticació ha acabat correctament. Ja podeu compartir amb Flattr des de l\'aplicació.</string> <string name="no_flattr_token_title">No s\'ha trobat cap testimoni Flattr</string> + <string name="no_flattr_token_notification_msg">Sembla que el compte flattr no està vinculat amb AntennaPod. Toqueu aquà per autenticar-vos.</string> <string name="no_flattr_token_msg">Sembla que el vostre compte de Flattr no està vinculat amb AntennaPod. Podeu connectar el vostre compte Flattr amb AntennaPod per a compartir continguts des de l\'aplicació, o bé accediu a la plana web de Flattr i compartiu els continguts des d\'allà .</string> <string name="authenticate_now_label">Autentica</string> <string name="action_forbidden_title">L\'acció no és permesa</string> @@ -199,6 +203,7 @@ <string name="pref_revokeAccess_title">Revoca l\'accés</string> <string name="pref_revokeAccess_sum">Revoqueu el permÃs d\'accés d\'aquesta aplicació al vostre compte Flattr.</string> <string name="pref_auto_flattr_title">Flattr automà tic</string> + <string name="pref_auto_flattr_sum">Configura la compartició automà tica per Flattr</string> <string name="user_interface_label">InterfÃcie d\'usuari</string> <string name="pref_set_theme_title">Selecció de tema</string> <string name="pref_set_theme_sum">Canvieu l\'aparença d\'AntennaPod.</string> @@ -221,9 +226,15 @@ <string name="pref_gpodnet_setlogin_information_sum">Canvia les dades d\'inici de sessió del vostre compte de gpodder.net</string> <string name="pref_playback_speed_title">Velocitats de reproducció</string> <string name="pref_playback_speed_sum">Personalitzeu les velocitats disponibles per a una velocitat de reproducció d\'à udio variable</string> + <string name="pref_seek_delta_title">Salta a l\'instant</string> + <string name="pref_seek_delta_sum">Salta aquesta quantitat de segons en rebobinar o en avançar rà pidament</string> <string name="pref_gpodnet_sethostname_title">Definex nom del servidor</string> <string name="pref_gpodnet_sethostname_use_default_host">Utilitza el servidor per defecte</string> <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Activa la compartició automà tica per Flattr</string> + <string name="auto_flattr_after_percent">Comparteix per Flattr l\'episodi en haver-ne reproduït el %d per cent</string> + <string name="auto_flattr_ater_beginning">Comparteix per Flattr l\'episodi en haver-ne iniciat la reproducció</string> + <string name="auto_flattr_ater_end">Comparteix per Flattr l\'episodi en acabar-se\'n la reproducció</string> <!--Search--> <string name="search_hint">Cerca canals o episodis</string> <string name="found_in_shownotes_label">Trobat a notes del programa</string> diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 76ecd8340..afc441b99 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -31,6 +31,7 @@ <string name="copy_url_label">Copier l\'URL</string> <string name="share_url_label">Partager l\'URL</string> <string name="copied_url_msg">URL copiée dans le presse-papier</string> + <string name="go_to_position_label">Aller à cette position</string> <!--Playback history--> <string name="clear_history_label">Effacer le journal</string> <!--Other--> @@ -59,6 +60,7 @@ <string name="auto_download_label">Télécharger automatiquement à l\'avenir</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL du flux</string> + <string name="etxtFeedurlHint">URL ou flux ou site web</string> <string name="txtvfeedurl_label">Ajouter un podcast par son URL</string> <string name="podcastdirectories_label">Trouver le podcast dans la bibliothèque</string> <string name="podcastdirectories_descr">Vous pouvez chercher de nouveaux podcasts en filtrant par nom, catégorie ou popularité dans la bibliothèque gpodder.net</string> @@ -133,6 +135,7 @@ <string name="position_default_label">00:00:00</string> <string name="player_buffering_msg">Mise en mémoire</string> <string name="playbackservice_notification_title">Lecture de podcast en cours</string> + <string name="unknown_media_key">AntennaPod - Touche média inconnue : %1$d</string> <!--Queue operations--> <string name="clear_queue_label">Effacer la liste</string> <string name="undo">Annuler</string> @@ -146,6 +149,7 @@ <string name="return_home_label">Revenir au départ</string> <string name="flattr_auth_success">L\'authentification a réussi. Vous pouvez maintenant flattr depuis cette application.</string> <string name="no_flattr_token_title">Aucun jeton Flattr trouvé.</string> + <string name="no_flattr_token_notification_msg">Votre compte flattr semble ne pas être connecté à AntennaPod. Touchez ici pour vous connecter.</string> <string name="no_flattr_token_msg">Votre compte Flattr se semble pas être connecté à AntennaPod. Vous pouvez soit connecter votre compte Flattr à AntennaPod pour pouvoir flattr depuis l\'application, ou vous pouvez aller sur le site de ce que vous voulez flattr.</string> <string name="authenticate_now_label">S\'authentifier</string> <string name="action_forbidden_title">Action interdite</string> @@ -199,6 +203,7 @@ <string name="pref_revokeAccess_title">Révoquer l\'accès</string> <string name="pref_revokeAccess_sum">Révoquer la permission d\'accès à votre compte Flattr depuis cette application.</string> <string name="pref_auto_flattr_title">Flattr automatique</string> + <string name="pref_auto_flattr_sum">Configurer les paiements flattr automatiques</string> <string name="user_interface_label">Interface utilisateur</string> <string name="pref_set_theme_title">Choisir un thème</string> <string name="pref_set_theme_sum">Modifier l\'apparence d\'AntennaPod.</string> @@ -221,9 +226,14 @@ <string name="pref_gpodnet_setlogin_information_sum">Modifier les information de connexion pour votre compte gpodder.net</string> <string name="pref_playback_speed_title">Vitesses de lecture</string> <string name="pref_playback_speed_sum">Modifier la liste des vitesses disponibles pour la lecture audio</string> + <string name="pref_seek_delta_sum">Bouger d\'autant de secondes en rembobinant ou en faisant une avance rapide </string> <string name="pref_gpodnet_sethostname_title">Choisir un nom de domaine</string> <string name="pref_gpodnet_sethostname_use_default_host">Utiliser le nom de domaine par défaut</string> <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Activer le paiement flattr automatique</string> + <string name="auto_flattr_after_percent">Lancer un paiement flattr pour un épisode dès que %d de l\'épisode a été joué</string> + <string name="auto_flattr_ater_beginning">Lancer le paiement flattr d\'un épisode dès que la lecture commence</string> + <string name="auto_flattr_ater_end">Lancer le paiement flattr d\'un épisode à la fin de la lecture</string> <!--Search--> <string name="search_hint">Chercher des flux ou épisodes</string> <string name="found_in_shownotes_label">Trouvé dans les notes</string> diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 1a49ee1d5..f1e525384 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -31,6 +31,7 @@ <string name="copy_url_label">Copiar URL</string> <string name="share_url_label">Partilhar URL</string> <string name="copied_url_msg">URL copiado para a área de transferência.</string> + <string name="go_to_position_label">Ir para esta posição</string> <!--Playback history--> <string name="clear_history_label">Limpar histórico</string> <!--Other--> @@ -59,6 +60,7 @@ <string name="auto_download_label">Incluir nas transferências automáticas</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL da fonte</string> + <string name="etxtFeedurlHint">URL da fonte ou sÃtio web</string> <string name="txtvfeedurl_label">Adicionar podcast via URL</string> <string name="podcastdirectories_label">Localizar podcasts no diretório</string> <string name="podcastdirectories_descr">Pode procurar os novos podcasts no gPodder.net por nome, categoria ou popularidade.</string> @@ -133,6 +135,7 @@ <string name="position_default_label">00:00:00</string> <string name="player_buffering_msg">A processar...</string> <string name="playbackservice_notification_title">Reproduzir podcast</string> + <string name="unknown_media_key">Tecla multimédia desconhecida: %1$d</string> <!--Queue operations--> <string name="clear_queue_label">Limpar fila</string> <string name="undo">Anular</string> @@ -146,6 +149,7 @@ <string name="return_home_label">Voltar ao ecrã</string> <string name="flattr_auth_success">Autenticação efetuada! Já pode fazer o flattr com a aplicação.</string> <string name="no_flattr_token_title">Token flattr não encontrado</string> + <string name="no_flattr_token_notification_msg">Parece que a sua conta flattr não está integrada ao AntennaPod. Clique aqui para autenticar.</string> <string name="no_flattr_token_msg">Parece que a sua conta flattr não está vinculada ao AntennaPod. Pode vincular a sua conta ao AntennaPod ou aceder ao sÃtio web para fazer o flattr.</string> <string name="authenticate_now_label">Autenticar</string> <string name="action_forbidden_title">Ação negada</string> @@ -199,6 +203,7 @@ <string name="pref_revokeAccess_title">Revogar acesso</string> <string name="pref_revokeAccess_sum">Revogar permissões de acesso da aplicação à sua conta flattr.</string> <string name="pref_auto_flattr_title">Flattr automático</string> + <string name="pref_auto_flattr_sum">Configurar flattr automático</string> <string name="user_interface_label">Interface</string> <string name="pref_set_theme_title">Tema</string> <string name="pref_set_theme_sum">Mudar o aspeto do AntennaPod.</string> @@ -221,9 +226,15 @@ <string name="pref_gpodnet_setlogin_information_sum">Mudar informação de acesso à sua conta gpodder.net.</string> <string name="pref_playback_speed_title">Velocidades de reprodução</string> <string name="pref_playback_speed_sum">Personalize as velocidades de reprodução disponÃveis.</string> + <string name="pref_seek_delta_title">Intervalo de procura</string> + <string name="pref_seek_delta_sum">Ao recuar ou avançar, procurar este valor de segundos</string> <string name="pref_gpodnet_sethostname_title">Definir nome de servidor</string> <string name="pref_gpodnet_sethostname_use_default_host">Utilizar pré-definição</string> <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Ativar flattr automático</string> + <string name="auto_flattr_after_percent">Flattr de episódios ao atingir %d porcento de reprodução</string> + <string name="auto_flattr_ater_beginning">Flattr de episodios ao iniciar a reprodução</string> + <string name="auto_flattr_ater_end">Flattr de episódios ao terminar a reprodução</string> <!--Search--> <string name="search_hint">Procurar fontes ou episódios</string> <string name="found_in_shownotes_label">Encontrado nas notas</string> @@ -326,4 +337,5 @@ <string name="authentication_label">Autenticação</string> <string name="authentication_descr">Altere o seu nome de utilizador e senha para este podcast e seus episódios.</string> <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importar subscrições de aplicações single-purpose...</string> </resources> diff --git a/res/values-sv-rSE/strings.xml b/res/values-sv-rSE/strings.xml index beda2187e..e17f54fa5 100644 --- a/res/values-sv-rSE/strings.xml +++ b/res/values-sv-rSE/strings.xml @@ -31,6 +31,7 @@ <string name="copy_url_label">Kopiera URL</string> <string name="share_url_label">Dela URL</string> <string name="copied_url_msg">Kopierade URL till clipboard.</string> + <string name="go_to_position_label">GÃ¥ hit</string> <!--Playback history--> <string name="clear_history_label">Rensa historik</string> <!--Other--> @@ -59,6 +60,7 @@ <string name="auto_download_label">Inkludera i automatiska nedladdningar</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Flödets URL</string> + <string name="etxtFeedurlHint">URL till flöde eller webbsida</string> <string name="txtvfeedurl_label">Lägg till podcast via URL</string> <string name="podcastdirectories_label">Hitta podcast i mapp</string> <string name="podcastdirectories_descr">Du kan söka efter podcasts baserat pÃ¥ namn, kategori eller populäritet pÃ¥ tjänsten gpodder.net</string> @@ -133,6 +135,7 @@ <string name="position_default_label">00:00:00</string> <string name="player_buffering_msg">Buffrar</string> <string name="playbackservice_notification_title">Spelar podcast</string> + <string name="unknown_media_key">AntannaPod - Okänd mediaknapp: %1$d</string> <!--Queue operations--> <string name="clear_queue_label">Rensa kön</string> <string name="undo">Ã…ngra</string> @@ -146,6 +149,7 @@ <string name="return_home_label">Ã…tergÃ¥ till Startsidan</string> <string name="flattr_auth_success">Autentiseringen lyckades! Du kan nu Flattra saker i appen.</string> <string name="no_flattr_token_title">Ingen Flattr token hittades</string> + <string name="no_flattr_token_notification_msg">Ditt Flattr-konto verkar inte vara anslutet till AntennaPod. Tryck här för att autentisera.</string> <string name="no_flattr_token_msg">Ditt Flattr konto verkar inte vara ansluten till AntennaPod. Du kan antingen ansluta ditt konto till AntennaPod att Flattr saker i app eller sÃ¥ kan du besöka webbplatsen för att Flattr det där.</string> <string name="authenticate_now_label">Autentisera</string> <string name="action_forbidden_title">Ã…tgärd förbjuden</string> @@ -167,7 +171,7 @@ <!--Variable Speed--> <string name="download_plugin_label">Ladda ner tillägg</string> <string name="no_playback_plugin_title">Tillägg ej installerat</string> - <string name="no_playback_plugin_msg">För att variabel uppspelningshastighet skall fungera mÃ¥ste ett tredjepartstillägg installeras.\n\nTryck pÃ¥ \'Ladda ner tillägg\' för att ladda ner ett gratis tilläg frÃ¥n Play store.\n\nAntennaPod ansvarar inte för problem med detta tillägg och de bör rapporteras till tilläggets skapare.\n</string> + <string name="no_playback_plugin_msg">För att variabel uppspelningshastighet skall fungera mÃ¥ste ett tredjepartstillägg installeras.\n\nTryck pÃ¥ \'Ladda ner tillägg\' för att ladda ner ett gratis tillägg frÃ¥n Play Store.\n\nAntennaPod ansvarar inte för problem med detta tillägg och de bör rapporteras till tilläggets skapare.</string> <string name="set_playback_speed_label">Uppspelningshastigheter</string> <!--Empty list labels--> <string name="no_items_label">Det finns inget i denna lista.</string> @@ -199,6 +203,7 @@ <string name="pref_revokeAccess_title">Ã…terkalla Ã¥tkomst</string> <string name="pref_revokeAccess_sum">Ã…terkalla behörigheten till ditt Flattr-konto för denna app.</string> <string name="pref_auto_flattr_title">Automatisk Flattring</string> + <string name="pref_auto_flattr_sum">Konfigurerar automatisk Flattring</string> <string name="user_interface_label">Användargränssnitt</string> <string name="pref_set_theme_title">Välj tema</string> <string name="pref_set_theme_sum">Ändra utseendet pÃ¥ AntennaPod.</string> @@ -221,9 +226,15 @@ <string name="pref_gpodnet_setlogin_information_sum">Ändra inloggningsinformationen för ditt gpodder.net konto.</string> <string name="pref_playback_speed_title">Uppspelningshastigheter</string> <string name="pref_playback_speed_sum">Anpassa de tillgängliga hastigheterna för variabel uppspelningshastighet.</string> + <string name="pref_seek_delta_title">Söktid</string> + <string name="pref_seek_delta_sum">Sök sÃ¥ här mÃ¥nga sekunder vid snabbspolning bakÃ¥t eller framÃ¥t</string> <string name="pref_gpodnet_sethostname_title">Sätt värdnamn</string> <string name="pref_gpodnet_sethostname_use_default_host">Använd standardvärden</string> <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Aktivera automatisk Flattring</string> + <string name="auto_flattr_after_percent">Flattra episoden sÃ¥ snart %d procent har spelats</string> + <string name="auto_flattr_ater_beginning">Flattra episoden när den startas</string> + <string name="auto_flattr_ater_end">Flattra episoden när den spelats klart</string> <!--Search--> <string name="search_hint">Sök efter flöden eller avsnitt</string> <string name="found_in_shownotes_label">Hittad i shownotes</string> @@ -235,7 +246,7 @@ <string name="opml_import_txtv_button_lable">OPML-filer lÃ¥ter dig flytta dina podcasts frÃ¥n en podcatcher till en annan.</string> <string name="opml_import_explanation">Om du vill importera en OPML-fil, mÃ¥ste du placera den i följande katalog och tryck pÃ¥ knappen nedan för att starta importen.</string> <string name="start_import_label">PÃ¥börja importering</string> - <string name="opml_import_label">OPML import</string> + <string name="opml_import_label">Importera OPML-fil</string> <string name="opml_directory_error">FEL! </string> <string name="reading_opml_label">Läser OPML-fil</string> <string name="opml_reader_error">Ett fel har skett vid iläsning av opml dokumentet:</string> diff --git a/src/com/aocate/media/ServiceBackedMediaPlayer.java b/src/com/aocate/media/ServiceBackedMediaPlayer.java index ef4572d33..8d08867ef 100644 --- a/src/com/aocate/media/ServiceBackedMediaPlayer.java +++ b/src/com/aocate/media/ServiceBackedMediaPlayer.java @@ -11,6 +11,12 @@ // 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. +// +// ----------------------------------------------------------------------- +// Compared to the original version, this class been slightly modified so +// that any acquired WakeLocks are only held while the MediaPlayer is +// playing (see the stayAwake method for more details). + package com.aocate.media; @@ -40,6 +46,8 @@ import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; import com.aocate.presto.service.IPlayMedia_0_8; +import de.danoeh.antennapod.BuildConfig; + /** * Class for connecting to remote speed-altering, media playing Service * Note that there is unusually high coupling between MediaPlayer and this @@ -206,6 +214,7 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { void error(int what, int extra) { owningMediaPlayer.lock.lock(); Log.e(SBMP_TAG, "error(" + what + ", " + extra + ")"); + stayAwake(false); try { if (!this.isErroring) { this.isErroring = true; @@ -478,6 +487,7 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { e.printStackTrace(); ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); } + stayAwake(false); } /** @@ -581,6 +591,7 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { e.printStackTrace(); ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); } + stayAwake(false); } /** @@ -870,12 +881,28 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); // Since mode can't be changed on the fly, we have to allocate a new one this.mWakeLock = pm.newWakeLock(mode, this.getClass().getName()); + this.mWakeLock.setReferenceCounted(false); } this.mWakeLock.acquire(); } } + /** + * Changes the state of the WakeLock if it has been acquired. + * If no WakeLock has been acquired with setWakeMode, this method does nothing. + * */ + private void stayAwake(boolean awake) { + if (BuildConfig.DEBUG) Log.d(SBMP_TAG, "stayAwake(" + awake + ")"); + if (mWakeLock != null) { + if (awake && !mWakeLock.isHeld()) { + mWakeLock.acquire(); + } else if (!awake && mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + } + private IOnBufferingUpdateListenerCallback_0_8.Stub mOnBufferingUpdateCallback = null; private void setOnBufferingUpdateCallback(IPlayMedia_0_8 iface) { try { @@ -913,6 +940,7 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { public void onCompletion() throws RemoteException { owningMediaPlayer.lock.lock(); Log.d(SBMP_TAG, "onCompletionListener being called"); + stayAwake(false); try { if (owningMediaPlayer.onCompletionListener != null) { owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); @@ -940,7 +968,8 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { this.mOnErrorCallback = new IOnErrorListenerCallback_0_8.Stub() { public boolean onError(int what, int extra) throws RemoteException { owningMediaPlayer.lock.lock(); - try { + stayAwake(false); + try { if (owningMediaPlayer.onErrorListener != null) { return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); } @@ -1146,6 +1175,7 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { e.printStackTrace(); ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); } + stayAwake(true); } /** @@ -1166,5 +1196,6 @@ public class ServiceBackedMediaPlayer extends MediaPlayerImpl { e.printStackTrace(); ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); } + stayAwake(false); } }
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/AppConfig.java b/src/de/danoeh/antennapod/AppConfig.java index 0e12a350f..24f13d4a3 100644 --- a/src/de/danoeh/antennapod/AppConfig.java +++ b/src/de/danoeh/antennapod/AppConfig.java @@ -2,6 +2,6 @@ package de.danoeh.antennapod; public final class AppConfig { /** Should be used when setting User-Agent header for HTTP-requests. */ - public final static String USER_AGENT = "AntennaPod/0.9.9.2"; + public final static String USER_AGENT = "AntennaPod/0.9.9.4"; } diff --git a/src/de/danoeh/antennapod/PodcastApp.java b/src/de/danoeh/antennapod/PodcastApp.java index 4c4766327..74628f3d6 100644 --- a/src/de/danoeh/antennapod/PodcastApp.java +++ b/src/de/danoeh/antennapod/PodcastApp.java @@ -3,7 +3,6 @@ package de.danoeh.antennapod; import android.app.Application; import android.content.res.Configuration; import android.util.Log; -import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.preferences.PlaybackPreferences; import de.danoeh.antennapod.preferences.UserPreferences; @@ -36,13 +35,6 @@ public class PodcastApp extends Application { SPAUtil.sendSPAppsQueryFeedsIntent(this); } - @Override - public void onLowMemory() { - super.onLowMemory(); - Log.w(TAG, "Received onLowOnMemory warning. Cleaning image cache..."); - ImageLoader.getInstance().wipeImageCache(); - } - public static float getLogicalDensity() { return LOGICAL_DENSITY; } diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java index 6373ff240..18d27ddda 100644 --- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -31,7 +31,7 @@ import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChapterListAdapter; import de.danoeh.antennapod.adapter.NavListAdapter; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.EventDistributor; @@ -343,7 +343,6 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc } else { ft.add(R.id.contentView, currentlyShownFragment); } - ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); ft.disallowAddToBackStack(); ft.commit(); updateNavButtonDrawable(); @@ -381,8 +380,10 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc @Override public void run() { - ImageLoader.getInstance().loadThumbnailBitmap(media, - butNavLeft); + PicassoProvider.getMediaMetadataPicassoInstance(AudioplayerActivity.this) + .load(media.getImageUri()) + .fit() + .into(butNavLeft); } }); butNavLeft.setContentDescription(getString(buttonTexts[2])); @@ -396,9 +397,12 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc @Override public void run() { - ImageLoader.getInstance().loadThumbnailBitmap(media, - butNavLeft); + PicassoProvider.getMediaMetadataPicassoInstance(AudioplayerActivity.this) + .load(media.getImageUri()) + .fit() + .into(butNavLeft); } + }); butNavLeft.setContentDescription(getString(buttonTexts[2])); diff --git a/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java index e89f8d05c..a03fa7949 100644 --- a/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java +++ b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java @@ -9,11 +9,28 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; -import android.widget.*; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.examples.HtmlToPlainText; +import org.jsoup.nodes.Document; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter; -import de.danoeh.antennapod.asynctask.ImageDiskCache; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.Feed; @@ -21,15 +38,6 @@ import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; -import org.apache.commons.lang3.StringUtils; -import org.jsoup.Jsoup; -import org.jsoup.examples.HtmlToPlainText; -import org.jsoup.nodes.Document; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; /** * Default implementation of OnlineFeedViewActivity. Shows the downloaded feed's items with their descriptions, @@ -115,9 +123,12 @@ public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { subscribeButton = (Button) header.findViewById(R.id.butSubscribe); if (feed.getImage() != null) { - ImageDiskCache.getDefaultInstance().loadThumbnailBitmap(feed.getImage().getDownload_url(), cover, (int) getResources().getDimension( - R.dimen.thumbnail_length)); + PicassoProvider.getDefaultPicassoInstance(this) + .load(feed.getImage().getDownload_url()) + .fit() + .into(cover); } + title.setText(feed.getTitle()); author.setText(feed.getAuthor()); description.setText(feed.getDescription()); diff --git a/src/de/danoeh/antennapod/activity/FeedInfoActivity.java b/src/de/danoeh/antennapod/activity/FeedInfoActivity.java index 7f60d0b10..5cf187eb6 100644 --- a/src/de/danoeh/antennapod/activity/FeedInfoActivity.java +++ b/src/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -12,7 +12,7 @@ import android.view.MenuItem; import android.widget.*; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedPreferences; @@ -78,8 +78,10 @@ public class FeedInfoActivity extends ActionBarActivity { @Override public void run() { - ImageLoader.getInstance().loadThumbnailBitmap( - feed.getImage(), imgvCover); + PicassoProvider.getDefaultPicassoInstance(FeedInfoActivity.this) + .load(feed.getImageUri()) + .fit() + .into(imgvCover); } }); diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java index 13e7b8a82..2e5372b60 100644 --- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -502,18 +502,24 @@ public abstract class MediaplayerActivity extends ActionBarActivity @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, - txtvPosition); + if (controller != null) { + prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, + txtvPosition); + } } @Override public void onStartTrackingTouch(SeekBar seekBar) { - controller.onSeekBarStartTrackingTouch(seekBar); + if (controller != null) { + controller.onSeekBarStartTrackingTouch(seekBar); + } } @Override public void onStopTrackingTouch(SeekBar seekBar) { - controller.onSeekBarStopTrackingTouch(seekBar, prog); + if (controller != null) { + controller.onSeekBarStopTrackingTouch(seekBar, prog); + } } } diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java index c2bbe8e47..a21985bb8 100644 --- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -46,6 +46,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { private static final String TAG = "PreferenceActivity"; private static final String PREF_FLATTR_THIS_APP = "prefFlattrThisApp"; + private static final String PREF_FLATTR_SETTINGS = "prefFlattrSettings"; 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_PREFS = "prefAutoFlattrPrefs"; @@ -351,6 +352,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { boolean hasFlattrToken = FlattrUtils.hasToken(); + findPreference(PREF_FLATTR_SETTINGS).setEnabled(FlattrUtils.hasAPICredentials()); findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); findPreference(PREF_AUTO_FLATTR_PREFS).setEnabled(hasFlattrToken); diff --git a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java index 46fa98c49..81661a288 100644 --- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -6,12 +6,12 @@ import android.graphics.drawable.ColorDrawable; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.support.v4.view.WindowCompat; import android.util.Log; import android.util.Pair; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; +import android.view.Window; import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -50,10 +50,11 @@ public class VideoplayerActivity extends MediaplayerActivity { setTheme(R.style.Theme_AntennaPod_Dark); } + @SuppressLint("AppCompatMethod") @Override protected void onCreate(Bundle savedInstanceState) { if (Build.VERSION.SDK_INT >= 11) { - supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); } getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onCreate(savedInstanceState); diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java index cb6dc41cf..6a60f65fe 100644 --- a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -270,8 +270,10 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity { @Override public void onClick(View v) { final int position = spinnerDevices.getSelectedItemPosition(); - selectedDevice = devices.get().get(position); - advance(); + if (position != AdapterView.INVALID_POSITION) { + selectedDevice = devices.get().get(position); + advance(); + } } }); } diff --git a/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java index 33b11774f..ef5af67de 100644 --- a/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java @@ -9,8 +9,9 @@ import android.widget.BaseAdapter; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.util.Converter; @@ -22,10 +23,13 @@ public class DownloadedEpisodesListAdapter extends BaseAdapter { private final Context context; private final ItemAccess itemAccess; + private final int imageSize; + public DownloadedEpisodesListAdapter(Context context, ItemAccess itemAccess) { super(); this.context = context; this.itemAccess = itemAccess; + this.imageSize = (int) context.getResources().getDimension(R.dimen.thumbnail_length_downloaded_item); } @Override @@ -83,12 +87,11 @@ public class DownloadedEpisodesListAdapter extends BaseAdapter { holder.butSecondary.setOnClickListener(secondaryActionListener); - ImageLoader.getInstance().loadThumbnailBitmap( - item, - holder.imageView, - (int) convertView.getResources().getDimension( - R.dimen.thumbnail_length) - ); + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.imageView); + return convertView; } diff --git a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java index 5e857c131..3f666eb8b 100644 --- a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java @@ -6,9 +6,14 @@ import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.*; +import android.widget.BaseExpandableListAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.DownloadRequester; @@ -19,276 +24,282 @@ import de.danoeh.antennapod.util.Converter; * structure of this list is: [header] [queueItems] [header] [unreadItems]. */ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { - private static final String TAG = "ExternalEpisodesListAdapter"; + private static final String TAG = "ExternalEpisodesListAdapter"; - public static final int GROUP_POS_QUEUE = 0; - public static final int GROUP_POS_UNREAD = 1; + public static final int GROUP_POS_QUEUE = 0; + public static final int GROUP_POS_UNREAD = 1; - private Context context; + private Context context; private ItemAccess itemAccess; - private ActionButtonCallback feedItemActionCallback; - private OnGroupActionClicked groupActionCallback; + private ActionButtonCallback feedItemActionCallback; + private OnGroupActionClicked groupActionCallback; + + private final int imageSize; - public ExternalEpisodesListAdapter(Context context, - ActionButtonCallback callback, - OnGroupActionClicked groupActionCallback, - ItemAccess itemAccess) { - super(); - this.context = context; + public ExternalEpisodesListAdapter(Context context, + ActionButtonCallback callback, + OnGroupActionClicked groupActionCallback, + ItemAccess itemAccess) { + super(); + this.context = context; this.itemAccess = itemAccess; - this.feedItemActionCallback = callback; - this.groupActionCallback = groupActionCallback; - } - - @Override - public boolean areAllItemsEnabled() { - return true; - } - - @Override - public FeedItem getChild(int groupPosition, int childPosition) { - if (groupPosition == GROUP_POS_QUEUE) { - return itemAccess.getQueueItemAt(childPosition); - } else if (groupPosition == GROUP_POS_UNREAD) { + this.feedItemActionCallback = callback; + this.groupActionCallback = groupActionCallback; + this.imageSize = (int) context.getResources().getDimension(R.dimen.thumbnail_length); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public FeedItem getChild(int groupPosition, int childPosition) { + if (groupPosition == GROUP_POS_QUEUE) { + return itemAccess.getQueueItemAt(childPosition); + } else if (groupPosition == GROUP_POS_UNREAD) { return itemAccess.getUnreadItemAt(childPosition); - } - return null; - } - - @Override - public long getChildId(int groupPosition, int childPosition) { - return childPosition; - } - - @Override - public View getChildView(int groupPosition, final int childPosition, - boolean isLastChild, View convertView, ViewGroup parent) { - Holder holder; - final FeedItem item = getChild(groupPosition, childPosition); - - if (convertView == null) { - holder = new Holder(); - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.external_itemlist_item, - parent, false); - holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); - holder.feedTitle = (TextView) convertView - .findViewById(R.id.txtvFeedname); - holder.lenSize = (TextView) convertView - .findViewById(R.id.txtvLenSize); - holder.downloadStatus = (ImageView) convertView - .findViewById(R.id.imgvDownloadStatus); - holder.feedImage = (ImageView) convertView - .findViewById(R.id.imgvFeedimage); - holder.butAction = (ImageButton) convertView - .findViewById(R.id.butAction); - holder.statusPlaying = (View) convertView - .findViewById(R.id.statusPlaying); - holder.episodeProgress = (ProgressBar) convertView - .findViewById(R.id.pbar_episode_progress); - convertView.setTag(holder); - } else { - holder = (Holder) convertView.getTag(); - } - - holder.title.setText(item.getTitle()); - holder.feedTitle.setText(item.getFeed().getTitle()); - FeedItem.State state = item.getState(); - - if (groupPosition == GROUP_POS_QUEUE) { - switch (state) { - case PLAYING: - holder.statusPlaying.setVisibility(View.VISIBLE); - holder.episodeProgress.setVisibility(View.VISIBLE); - break; - case IN_PROGRESS: - holder.statusPlaying.setVisibility(View.GONE); - holder.episodeProgress.setVisibility(View.VISIBLE); - break; - case NEW: - holder.statusPlaying.setVisibility(View.GONE); - holder.episodeProgress.setVisibility(View.GONE); - break; - default: - holder.statusPlaying.setVisibility(View.GONE); - holder.episodeProgress.setVisibility(View.GONE); - break; - } - } else { - holder.statusPlaying.setVisibility(View.GONE); - holder.episodeProgress.setVisibility(View.GONE); - } - - FeedMedia media = item.getMedia(); - if (media != null) { - - if (state == FeedItem.State.PLAYING - || state == FeedItem.State.IN_PROGRESS) { - if (media.getDuration() > 0) { - holder.episodeProgress.setProgress((int) (((double) media - .getPosition()) / media.getDuration() * 100)); - holder.lenSize.setText(Converter - .getDurationStringLong(media.getDuration() - - media.getPosition())); - } - } else if (!media.isDownloaded()) { - holder.lenSize.setText(context.getString(R.string.size_prefix) - + Converter.byteToString(media.getSize())); - } else { - holder.lenSize.setText(context - .getString(R.string.length_prefix) - + Converter.getDurationStringLong(media.getDuration())); - } - - 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)); + } + return null; + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public View getChildView(int groupPosition, final int childPosition, + boolean isLastChild, View convertView, ViewGroup parent) { + Holder holder; + final FeedItem item = getChild(groupPosition, childPosition); + + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.external_itemlist_item, + parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.feedTitle = (TextView) convertView + .findViewById(R.id.txtvFeedname); + holder.lenSize = (TextView) convertView + .findViewById(R.id.txtvLenSize); + holder.downloadStatus = (ImageView) convertView + .findViewById(R.id.imgvDownloadStatus); + holder.feedImage = (ImageView) convertView + .findViewById(R.id.imgvFeedimage); + holder.butAction = (ImageButton) convertView + .findViewById(R.id.butAction); + holder.statusPlaying = (View) convertView + .findViewById(R.id.statusPlaying); + holder.episodeProgress = (ProgressBar) convertView + .findViewById(R.id.pbar_episode_progress); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + holder.feedTitle.setText(item.getFeed().getTitle()); + FeedItem.State state = item.getState(); + + if (groupPosition == GROUP_POS_QUEUE) { + switch (state) { + case PLAYING: + holder.statusPlaying.setVisibility(View.VISIBLE); + holder.episodeProgress.setVisibility(View.VISIBLE); + break; + case IN_PROGRESS: + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.VISIBLE); + break; + case NEW: + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.GONE); + break; + default: + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.GONE); + break; + } + } else { + holder.statusPlaying.setVisibility(View.GONE); + holder.episodeProgress.setVisibility(View.GONE); + } + + FeedMedia media = item.getMedia(); + if (media != null) { + + if (state == FeedItem.State.PLAYING + || state == FeedItem.State.IN_PROGRESS) { + if (media.getDuration() > 0) { + holder.episodeProgress.setProgress((int) (((double) media + .getPosition()) / media.getDuration() * 100)); + holder.lenSize.setText(Converter + .getDurationStringLong(media.getDuration() + - media.getPosition())); + } + } else if (!media.isDownloaded()) { + holder.lenSize.setText(context.getString(R.string.size_prefix) + + Converter.byteToString(media.getSize())); + } else { + holder.lenSize.setText(context + .getString(R.string.length_prefix) + + Converter.getDurationStringLong(media.getDuration())); + } + + 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); - } - } else { - holder.downloadStatus.setVisibility(View.VISIBLE); - holder.downloadStatus - .setImageDrawable(drawables.getDrawable(0)); + } else { + holder.downloadStatus.setVisibility(View.INVISIBLE); + } + } else { + holder.downloadStatus.setVisibility(View.VISIBLE); + holder.downloadStatus + .setImageDrawable(drawables.getDrawable(0)); holder.downloadStatus.setContentDescription(context.getString(labels[0])); - } - } else { - holder.downloadStatus.setVisibility(View.INVISIBLE); - holder.lenSize.setVisibility(View.INVISIBLE); - } - - ImageLoader.getInstance().loadThumbnailBitmap( - item, - holder.feedImage, - (int) convertView.getResources().getDimension( - R.dimen.thumbnail_length)); - holder.butAction.setFocusable(false); - holder.butAction.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - feedItemActionCallback.onActionButtonPressed(item); - } - }); - - return convertView; - - } - - static class Holder { - TextView title; - TextView feedTitle; - TextView lenSize; - ImageView downloadStatus; - ImageView feedImage; - ImageButton butAction; - View statusPlaying; - ProgressBar episodeProgress; - } - - @Override - public int getChildrenCount(int groupPosition) { - if (groupPosition == GROUP_POS_QUEUE) { - return itemAccess.getQueueSize(); - } else if (groupPosition == GROUP_POS_UNREAD) { - return itemAccess.getUnreadItemsSize(); - } - return 0; - } - - @Override - public int getGroupCount() { - // Hide 'unread items' group if empty - if (itemAccess.getUnreadItemsSize() > 0) { - return 2; - } else { - return 1; - } - } - - @Override - public long getGroupId(int groupPosition) { - return groupPosition; - } - - @Override - public View getGroupView(final int groupPosition, boolean isExpanded, - View convertView, ViewGroup parent) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.feeditemlist_header, parent, false); - TextView headerTitle = (TextView) convertView - .findViewById(0); - ImageButton actionButton = (ImageButton) convertView - .findViewById(R.id.butAction); - TextView numItems = (TextView) convertView.findViewById(0); - - String headerString = null; - int childrenCount = 0; - - if (groupPosition == 0) { - headerString = context.getString(R.string.queue_label); - childrenCount = getChildrenCount(GROUP_POS_QUEUE); - } else { - headerString = context.getString(R.string.waiting_list_label); - childrenCount = getChildrenCount(GROUP_POS_UNREAD); - } - headerTitle.setText(headerString); - if (childrenCount <= 0) { - numItems.setVisibility(View.INVISIBLE); - } else { - numItems.setVisibility(View.VISIBLE); - numItems.setText(Integer.toString(childrenCount)); - } - actionButton.setFocusable(false); - actionButton.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - groupActionCallback.onClick(getGroupId(groupPosition)); - } - }); - return convertView; - } - - @Override - public boolean isEmpty() { - return itemAccess.getUnreadItemsSize() == 0 - && itemAccess.getQueueSize() == 0; - } - - @Override - public Object getGroup(int groupPosition) { - return null; - } - - @Override - public boolean hasStableIds() { - return true; - } - - @Override - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } - - public interface OnGroupActionClicked { - public void onClick(long groupId); - } + } + } else { + holder.downloadStatus.setVisibility(View.INVISIBLE); + holder.lenSize.setVisibility(View.INVISIBLE); + } + + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.feedImage); + + holder.butAction.setFocusable(false); + holder.butAction.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + feedItemActionCallback.onActionButtonPressed(item); + } + }); + + return convertView; + + } + + static class Holder { + TextView title; + TextView feedTitle; + TextView lenSize; + ImageView downloadStatus; + ImageView feedImage; + ImageButton butAction; + View statusPlaying; + ProgressBar episodeProgress; + } + + @Override + public int getChildrenCount(int groupPosition) { + if (groupPosition == GROUP_POS_QUEUE) { + return itemAccess.getQueueSize(); + } else if (groupPosition == GROUP_POS_UNREAD) { + return itemAccess.getUnreadItemsSize(); + } + return 0; + } + + @Override + public int getGroupCount() { + // Hide 'unread items' group if empty + if (itemAccess.getUnreadItemsSize() > 0) { + return 2; + } else { + return 1; + } + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public View getGroupView(final int groupPosition, boolean isExpanded, + View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.feeditemlist_header, parent, false); + TextView headerTitle = (TextView) convertView + .findViewById(0); + ImageButton actionButton = (ImageButton) convertView + .findViewById(R.id.butAction); + TextView numItems = (TextView) convertView.findViewById(0); + + String headerString = null; + int childrenCount = 0; + + if (groupPosition == 0) { + headerString = context.getString(R.string.queue_label); + childrenCount = getChildrenCount(GROUP_POS_QUEUE); + } else { + headerString = context.getString(R.string.waiting_list_label); + childrenCount = getChildrenCount(GROUP_POS_UNREAD); + } + headerTitle.setText(headerString); + if (childrenCount <= 0) { + numItems.setVisibility(View.INVISIBLE); + } else { + numItems.setVisibility(View.VISIBLE); + numItems.setText(Integer.toString(childrenCount)); + } + actionButton.setFocusable(false); + actionButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + groupActionCallback.onClick(getGroupId(groupPosition)); + } + }); + return convertView; + } + + @Override + public boolean isEmpty() { + return itemAccess.getUnreadItemsSize() == 0 + && itemAccess.getQueueSize() == 0; + } + + @Override + public Object getGroup(int groupPosition) { + return null; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + public interface OnGroupActionClicked { + public void onClick(long groupId); + } public static interface ItemAccess { public int getQueueSize(); + public int getUnreadItemsSize(); + public FeedItem getQueueItemAt(int position); + public FeedItem getUnreadItemAt(int position); } diff --git a/src/de/danoeh/antennapod/adapter/NavListAdapter.java b/src/de/danoeh/antennapod/adapter/NavListAdapter.java index 9676372fb..ef8e8ce07 100644 --- a/src/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -11,7 +11,7 @@ import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.feed.Feed; /** @@ -189,7 +189,11 @@ public class NavListAdapter extends BaseAdapter { } holder.title.setText(feed.getTitle()); - ImageLoader.getInstance().loadThumbnailBitmap(feed.getImage(), holder.image, (int) context.getResources().getDimension(R.dimen.thumbnail_length_navlist)); + + PicassoProvider.getDefaultPicassoInstance(context) + .load(feed.getImageUri()) + .fit() + .into(holder.image); return convertView; } diff --git a/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java index 07fd3e6b1..8abe49133 100644 --- a/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java @@ -5,9 +5,14 @@ import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.*; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.DownloadRequester; @@ -124,13 +129,11 @@ public class NewEpisodesListAdapter extends BaseAdapter { holder.butSecondary.setTag(item); holder.butSecondary.setOnClickListener(secondaryActionListener); + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.imageView); - ImageLoader.getInstance().loadThumbnailBitmap( - item, - holder.imageView, - (int) convertView.getResources().getDimension( - R.dimen.thumbnail_length) - ); return convertView; } diff --git a/src/de/danoeh/antennapod/adapter/QueueListAdapter.java b/src/de/danoeh/antennapod/adapter/QueueListAdapter.java index f671ba5c6..ebe519592 100644 --- a/src/de/danoeh/antennapod/adapter/QueueListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/QueueListAdapter.java @@ -6,7 +6,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.*; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.DownloadRequester; @@ -22,13 +22,13 @@ public class QueueListAdapter extends BaseAdapter { private final ActionButtonCallback actionButtonCallback; private final ActionButtonUtils actionButtonUtils; + public QueueListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback) { super(); this.context = context; this.itemAccess = itemAccess; this.actionButtonUtils = new ActionButtonUtils(context); this.actionButtonCallback = actionButtonCallback; - } @Override @@ -92,13 +92,11 @@ public class QueueListAdapter extends BaseAdapter { holder.butSecondary.setTag(item); holder.butSecondary.setOnClickListener(secondaryActionListener); + PicassoProvider.getMediaMetadataPicassoInstance(context) + .load(item.getImageUri()) + .fit() + .into(holder.imageView); - ImageLoader.getInstance().loadThumbnailBitmap( - item, - holder.imageView, - (int) convertView.getResources().getDimension( - R.dimen.thumbnail_length) - ); return convertView; } diff --git a/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java b/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java index ecfbb4660..2314c2269 100644 --- a/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java @@ -4,25 +4,26 @@ import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedComponent; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.SearchResult; -import java.util.List; - -/** List adapter for search activity. */ +/** + * List adapter for search activity. + */ public class SearchlistAdapter extends BaseAdapter { - private final Context context; + private final Context context; private final ItemAccess itemAccess; + public SearchlistAdapter(Context context, ItemAccess itemAccess) { this.context = context; this.itemAccess = itemAccess; @@ -44,61 +45,65 @@ public class SearchlistAdapter extends BaseAdapter { } @Override - public View getView(int position, View convertView, ViewGroup parent) { - final Holder holder; - SearchResult result = getItem(position); - FeedComponent component = result.getComponent(); - - // Inflate Layout - if (convertView == null) { - holder = new Holder(); - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - convertView = inflater.inflate(R.layout.searchlist_item, parent, false); - holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); - holder.cover = (ImageView) convertView - .findViewById(R.id.imgvFeedimage); - holder.subtitle = (TextView) convertView - .findViewById(R.id.txtvSubtitle); - - convertView.setTag(holder); - } else { - holder = (Holder) convertView.getTag(); - } - if (component.getClass() == Feed.class) { - final Feed feed = (Feed) component; - holder.title.setText(feed.getTitle()); - holder.subtitle.setVisibility(View.GONE); - ImageLoader.getInstance().loadThumbnailBitmap(feed.getImage(), - holder.cover, (int) convertView.getResources().getDimension(R.dimen.thumbnail_length)); - } else if (component.getClass() == FeedItem.class) { - final FeedItem item = (FeedItem) component; - holder.title.setText(item.getTitle()); - if (result.getSubtitle() != null) { - holder.subtitle.setVisibility(View.VISIBLE); - holder.subtitle.setText(result.getSubtitle()); - } - - ImageLoader.getInstance().loadThumbnailBitmap( - item.getFeed().getImage(), - holder.cover, - (int) convertView.getResources().getDimension( - R.dimen.thumbnail_length)); - - } - - return convertView; - } - - static class Holder { - ImageView cover; - TextView title; - TextView subtitle; - } + public View getView(int position, View convertView, ViewGroup parent) { + final Holder holder; + SearchResult result = getItem(position); + FeedComponent component = result.getComponent(); + + // Inflate Layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.searchlist_item, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.cover = (ImageView) convertView + .findViewById(R.id.imgvFeedimage); + holder.subtitle = (TextView) convertView + .findViewById(R.id.txtvSubtitle); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + if (component.getClass() == Feed.class) { + final Feed feed = (Feed) component; + holder.title.setText(feed.getTitle()); + holder.subtitle.setVisibility(View.GONE); + + PicassoProvider.getDefaultPicassoInstance(context) + .load(feed.getImageUri()) + .fit() + .into(holder.cover); + + } else if (component.getClass() == FeedItem.class) { + final FeedItem item = (FeedItem) component; + holder.title.setText(item.getTitle()); + if (result.getSubtitle() != null) { + holder.subtitle.setVisibility(View.VISIBLE); + holder.subtitle.setText(result.getSubtitle()); + } + + PicassoProvider.getDefaultPicassoInstance(context) + .load(item.getFeed().getImageUri()) + .fit() + .into(holder.cover); + + } + + return convertView; + } + + static class Holder { + ImageView cover; + TextView title; + TextView subtitle; + } public static interface ItemAccess { int getCount(); + SearchResult getItem(int position); } diff --git a/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java index f20232a6f..f2e78a57e 100644 --- a/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -7,23 +7,20 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageDiskCache; -import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; import java.util.List; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.asynctask.PicassoProvider; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; + /** * Adapter for displaying a list of GPodnetPodcast-Objects. */ public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { - private final ImageDiskCache diskCache; - private final int thumbnailLength; public PodcastListAdapter(Context context, int resource, List<GpodnetPodcast> objects) { super(context, resource, objects); - diskCache = ImageDiskCache.getDefaultInstance(); - thumbnailLength = (int) context.getResources().getDimension(R.dimen.thumbnail_length); } @Override @@ -50,7 +47,11 @@ public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { holder.title.setText(podcast.getTitle()); holder.description.setText(podcast.getDescription()); - diskCache.loadThumbnailBitmap(podcast.getLogoUrl(), holder.image, thumbnailLength); + + PicassoProvider.getDefaultPicassoInstance(convertView.getContext()) + .load(podcast.getLogoUrl()) + .fit() + .into(holder.image); return convertView; } diff --git a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java b/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java deleted file mode 100644 index 43118c3af..000000000 --- a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java +++ /dev/null @@ -1,115 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.TransitionDrawable; -import android.os.Handler; -import android.util.Log; -import android.widget.ImageView; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader.ImageWorkerTaskResource; -import de.danoeh.antennapod.util.BitmapDecoder; - -public class BitmapDecodeWorkerTask extends Thread { - - protected int PREFERRED_LENGTH; - public static final int FADE_DURATION = 500; - - /** - * Can be thumbnail or cover - */ - protected int imageType; - - private static final String TAG = "BitmapDecodeWorkerTask"; - private ImageView target; - protected CachedBitmap cBitmap; - - protected ImageLoader.ImageWorkerTaskResource imageResource; - - private Handler handler; - - private final int defaultCoverResource; - - public BitmapDecodeWorkerTask(Handler handler, ImageView target, - ImageWorkerTaskResource imageResource, int length, int imageType) { - super(); - this.handler = handler; - this.target = target; - this.imageResource = imageResource; - this.PREFERRED_LENGTH = length; - this.imageType = imageType; - this.defaultCoverResource = android.R.color.transparent; - } - - /** - * Should return true if tag of the imageview is still the same it was - * before the bitmap was decoded - */ - protected boolean tagsMatching(ImageView target) { - Object tag = target.getTag(R.id.imageloader_key); - return tag != null && tag.equals(imageResource.getImageLoaderCacheKey()); - } - - protected void onPostExecute() { - // check if imageview is still supposed to display this image - if (tagsMatching(target) && cBitmap.getBitmap() != null) { - Drawable[] drawables = new Drawable[]{ - PodcastApp.getInstance().getResources().getDrawable(android.R.color.transparent), - new BitmapDrawable(PodcastApp.getInstance().getResources(), cBitmap.getBitmap()) - }; - TransitionDrawable transitionDrawable = new TransitionDrawable(drawables); - target.setImageDrawable(transitionDrawable); - transitionDrawable.startTransition(FADE_DURATION); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Not displaying image"); - } - } - - @Override - public void run() { - cBitmap = new CachedBitmap(BitmapDecoder.decodeBitmapFromWorkerTaskResource( - PREFERRED_LENGTH, imageResource), PREFERRED_LENGTH); - if (cBitmap.getBitmap() != null) { - storeBitmapInCache(cBitmap); - } else { - Log.w(TAG, "Could not load bitmap. Using default image."); - cBitmap = new CachedBitmap(BitmapFactory.decodeResource( - target.getResources(), defaultCoverResource), - PREFERRED_LENGTH); - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Finished loading bitmaps"); - - endBackgroundTask(); - } - - protected final void endBackgroundTask() { - handler.post(new Runnable() { - - @Override - public void run() { - onPostExecute(); - } - - }); - } - - protected void onInvalidStream() { - cBitmap = new CachedBitmap(BitmapFactory.decodeResource( - target.getResources(), defaultCoverResource), PREFERRED_LENGTH); - } - - protected void storeBitmapInCache(CachedBitmap cb) { - ImageLoader loader = ImageLoader.getInstance(); - if (imageType == ImageLoader.IMAGE_TYPE_COVER) { - loader.addBitmapToCoverCache(imageResource.getImageLoaderCacheKey(), cb); - } else if (imageType == ImageLoader.IMAGE_TYPE_THUMBNAIL) { - loader.addBitmapToThumbnailCache(imageResource.getImageLoaderCacheKey(), cb); - } - } - -} diff --git a/src/de/danoeh/antennapod/asynctask/CachedBitmap.java b/src/de/danoeh/antennapod/asynctask/CachedBitmap.java deleted file mode 100644 index 5a89b7b53..000000000 --- a/src/de/danoeh/antennapod/asynctask/CachedBitmap.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.graphics.Bitmap; - -/** Stores a bitmap and the length it was decoded with. */ -public class CachedBitmap { - - private Bitmap bitmap; - private int length; - - public CachedBitmap(Bitmap bitmap, int length) { - super(); - this.bitmap = bitmap; - this.length = length; - } - - public Bitmap getBitmap() { - return bitmap; - } - public int getLength() { - return length; - } - - - - -} diff --git a/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java deleted file mode 100644 index 77609f28b..000000000 --- a/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java +++ /dev/null @@ -1,397 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.os.Handler; -import android.util.Log; -import android.util.Pair; -import android.widget.ImageView; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.service.download.DownloadRequest; -import de.danoeh.antennapod.service.download.HttpDownloader; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; - -import java.io.*; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * Provides local cache for storing downloaded image. An image disk cache downloads images and stores them as long - * as the cache is not full. Once the cache is full, the image disk cache will delete older images. - */ -public class ImageDiskCache { - private static final String TAG = "ImageDiskCache"; - - private static HashMap<String, ImageDiskCache> cacheSingletons = new HashMap<String, ImageDiskCache>(); - - /** - * Return a default instance of an ImageDiskCache. This cache will store data in the external cache folder. - */ - public static synchronized ImageDiskCache getDefaultInstance() { - final String DEFAULT_PATH = "imagecache"; - final long DEFAULT_MAX_CACHE_SIZE = 10 * 1024 * 1024; - - File cacheDir = PodcastApp.getInstance().getExternalCacheDir(); - if (cacheDir == null) { - return null; - } - return getInstance(new File(cacheDir, DEFAULT_PATH).getAbsolutePath(), DEFAULT_MAX_CACHE_SIZE); - } - - /** - * Return an instance of an ImageDiskCache that stores images in the specified folder. - */ - public static synchronized ImageDiskCache getInstance(String path, long maxCacheSize) { - Validate.notNull(path); - - if (cacheSingletons.containsKey(path)) { - return cacheSingletons.get(path); - } - - ImageDiskCache cache = cacheSingletons.get(path); - if (cache == null) { - cache = new ImageDiskCache(path, maxCacheSize); - cacheSingletons.put(new File(path).getAbsolutePath(), cache); - } - cacheSingletons.put(path, cache); - return cache; - } - - /** - * Filename - cache object mapping - */ - private static final String CACHE_FILE_NAME = "cachefile"; - private ExecutorService executor; - private ConcurrentHashMap<String, DiskCacheObject> diskCache; - private final long maxCacheSize; - private int cacheSize; - private final File cacheFolder; - private Handler handler; - - private ImageDiskCache(String path, long maxCacheSize) { - this.maxCacheSize = maxCacheSize; - this.cacheFolder = new File(path); - if (!cacheFolder.exists() && !cacheFolder.mkdir()) { - throw new IllegalArgumentException("Image disk cache could not create cache folder in: " + path); - } - - executor = Executors.newFixedThreadPool(Runtime.getRuntime() - .availableProcessors()); - handler = new Handler(); - } - - private synchronized void initCacheFolder() { - if (diskCache == null) { - if (BuildConfig.DEBUG) Log.d(TAG, "Initializing cache folder"); - File cacheFile = new File(cacheFolder, CACHE_FILE_NAME); - if (cacheFile.exists()) { - try { - InputStream in = new FileInputStream(cacheFile); - BufferedInputStream buffer = new BufferedInputStream(in); - ObjectInputStream objectInput = new ObjectInputStream(buffer); - diskCache = (ConcurrentHashMap<String, DiskCacheObject>) objectInput.readObject(); - // calculate cache size - for (DiskCacheObject dco : diskCache.values()) { - cacheSize += dco.size; - } - deleteInvalidFiles(); - } catch (IOException e) { - e.printStackTrace(); - diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); - } catch (ClassCastException e) { - e.printStackTrace(); - diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); - } - } else { - diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); - } - } - } - - private List<File> getCacheFileList() { - Collection<DiskCacheObject> values = diskCache.values(); - List<File> files = new ArrayList<File>(); - for (DiskCacheObject dco : values) { - files.add(dco.getFile()); - } - files.add(new File(cacheFolder, CACHE_FILE_NAME)); - return files; - } - - private Pair<String, DiskCacheObject> getOldestCacheObject() { - Collection<String> keys = diskCache.keySet(); - DiskCacheObject oldest = null; - String oldestKey = null; - - for (String key : keys) { - - if (oldestKey == null) { - oldestKey = key; - oldest = diskCache.get(key); - } else { - DiskCacheObject dco = diskCache.get(key); - if (oldest.timestamp > dco.timestamp) { - oldestKey = key; - oldest = diskCache.get(key); - } - } - } - return new Pair<String, DiskCacheObject>(oldestKey, oldest); - } - - private synchronized void deleteCacheObject(String key, DiskCacheObject value) { - Log.i(TAG, "Deleting cached object: " + key); - diskCache.remove(key); - boolean result = value.getFile().delete(); - if (!result) { - Log.w(TAG, "Could not delete file " + value.fileUrl); - } - cacheSize -= value.size; - } - - private synchronized void deleteInvalidFiles() { - // delete files that are not stored inside the cache - File[] files = cacheFolder.listFiles(); - List<File> cacheFiles = getCacheFileList(); - for (File file : files) { - if (!cacheFiles.contains(file)) { - Log.i(TAG, "Deleting unused file: " + file.getAbsolutePath()); - boolean result = file.delete(); - if (!result) { - Log.w(TAG, "Could not delete file: " + file.getAbsolutePath()); - } - } - } - } - - private synchronized void cleanup() { - if (cacheSize > maxCacheSize) { - while (cacheSize > maxCacheSize) { - Pair<String, DiskCacheObject> oldest = getOldestCacheObject(); - deleteCacheObject(oldest.first, oldest.second); - } - } - } - - /** - * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will - * be loaded from the disk. Otherwise, the image will be downloaded first. - * The image will be stored in the thumbnail cache. - */ - public void loadThumbnailBitmap(final String url, final ImageView target, final int length) { - if (url == null) { - Log.w(TAG, "loadThumbnailBitmap: Call was ignored because url = null"); - return; - } - final ImageLoader il = ImageLoader.getInstance(); - target.setTag(R.id.image_disk_cache_key, url); - if (diskCache != null) { - DiskCacheObject dco = getFromCacheIfAvailable(url); - if (dco != null) { - il.loadThumbnailBitmap(dco.loadImage(), target, length); - return; - } - } - target.setImageResource(android.R.color.transparent); - executor.submit(new ImageDownloader(url) { - @Override - protected void onImageLoaded(DiskCacheObject diskCacheObject) { - final Object tag = target.getTag(R.id.image_disk_cache_key); - if (tag != null && StringUtils.equals((String) tag, url)) { - il.loadThumbnailBitmap(diskCacheObject.loadImage(), target, length); - } - } - }); - - } - - /** - * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will - * be loaded from the disk. Otherwise, the image will be downloaded first. - * The image will be stored in the cover cache. - */ - public void loadCoverBitmap(final String url, final ImageView target, final int length) { - if (url == null) { - Log.w(TAG, "loadCoverBitmap: Call was ignored because url = null"); - return; - } - final ImageLoader il = ImageLoader.getInstance(); - target.setTag(R.id.image_disk_cache_key, url); - if (diskCache != null) { - DiskCacheObject dco = getFromCacheIfAvailable(url); - if (dco != null) { - il.loadThumbnailBitmap(dco.loadImage(), target, length); - return; - } - } - target.setImageResource(android.R.color.transparent); - executor.submit(new ImageDownloader(url) { - @Override - protected void onImageLoaded(DiskCacheObject diskCacheObject) { - final Object tag = target.getTag(R.id.image_disk_cache_key); - if (tag != null && StringUtils.equals((String) tag, url)) { - il.loadCoverBitmap(diskCacheObject.loadImage(), target, length); - } - } - }); - } - - private synchronized void addToDiskCache(String url, DiskCacheObject obj) { - if (diskCache == null) { - initCacheFolder(); - } - if (BuildConfig.DEBUG) Log.d(TAG, "Adding new image to disk cache: " + url); - diskCache.put(url, obj); - cacheSize += obj.size; - if (cacheSize > maxCacheSize) { - cleanup(); - } - saveCacheInfoFile(); - } - - private synchronized void saveCacheInfoFile() { - OutputStream out = null; - try { - out = new BufferedOutputStream(new FileOutputStream(new File(cacheFolder, CACHE_FILE_NAME))); - ObjectOutputStream objOut = new ObjectOutputStream(out); - objOut.writeObject(diskCache); - } catch (IOException e) { - e.printStackTrace(); - } finally { - IOUtils.closeQuietly(out); - } - } - - private synchronized DiskCacheObject getFromCacheIfAvailable(String key) { - if (diskCache == null) { - initCacheFolder(); - } - DiskCacheObject dco = diskCache.get(key); - if (dco != null) { - dco.timestamp = System.currentTimeMillis(); - } - return dco; - } - - ConcurrentHashMap<String, File> runningDownloads = new ConcurrentHashMap<String, File>(); - - private abstract class ImageDownloader implements Runnable { - private String downloadUrl; - - public ImageDownloader(String downloadUrl) { - this.downloadUrl = downloadUrl; - } - - protected abstract void onImageLoaded(DiskCacheObject diskCacheObject); - - public void run() { - DiskCacheObject tmp = getFromCacheIfAvailable(downloadUrl); - if (tmp != null) { - onImageLoaded(tmp); - return; - } - - DiskCacheObject dco = null; - File newFile = new File(cacheFolder, Integer.toString(downloadUrl.hashCode())); - synchronized (ImageDiskCache.this) { - if (runningDownloads.containsKey(newFile.getAbsolutePath())) { - Log.d(TAG, "Download is already running: " + newFile.getAbsolutePath()); - return; - } else { - runningDownloads.put(newFile.getAbsolutePath(), newFile); - } - } - if (newFile.exists()) { - newFile.delete(); - } - - HttpDownloader result = downloadFile(newFile.getAbsolutePath(), downloadUrl); - if (result.getResult().isSuccessful()) { - long size = result.getDownloadRequest().getSoFar(); - - dco = new DiskCacheObject(newFile.getAbsolutePath(), size); - addToDiskCache(downloadUrl, dco); - if (BuildConfig.DEBUG) Log.d(TAG, "Image was downloaded"); - } else { - Log.w(TAG, "Download of url " + downloadUrl + " failed. Reason: " + result.getResult().getReasonDetailed() + "(" + result.getResult().getReason() + ")"); - } - - if (dco != null) { - final DiskCacheObject dcoRef = dco; - handler.post(new Runnable() { - @Override - public void run() { - onImageLoaded(dcoRef); - } - }); - - } - runningDownloads.remove(newFile.getAbsolutePath()); - - } - - private HttpDownloader downloadFile(String destination, String source) { - DownloadRequest request = new DownloadRequest(destination, source, "", 0, 0); - HttpDownloader downloader = new HttpDownloader(request); - downloader.call(); - return downloader; - } - } - - private static class DiskCacheObject implements Serializable { - private final String fileUrl; - - /** - * Last usage of this image cache object. - */ - private long timestamp; - private final long size; - - public DiskCacheObject(String fileUrl, long size) { - Validate.notNull(fileUrl); - this.fileUrl = fileUrl; - this.timestamp = System.currentTimeMillis(); - this.size = size; - } - - public File getFile() { - return new File(fileUrl); - } - - public ImageLoader.ImageWorkerTaskResource loadImage() { - return new ImageLoader.ImageWorkerTaskResource() { - - @Override - public InputStream openImageInputStream() { - try { - return new FileInputStream(getFile()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - return null; - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - IOUtils.closeQuietly(input); - return openImageInputStream(); - } - - @Override - public String getImageLoaderCacheKey() { - return fileUrl; - } - }; - } - } -} diff --git a/src/de/danoeh/antennapod/asynctask/ImageLoader.java b/src/de/danoeh/antennapod/asynctask/ImageLoader.java deleted file mode 100644 index 6c60b7b1f..000000000 --- a/src/de/danoeh/antennapod/asynctask/ImageLoader.java +++ /dev/null @@ -1,246 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.content.Context; -import android.os.Handler; -import android.support.v4.util.LruCache; -import android.util.Log; -import android.widget.ImageView; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; - -import java.io.InputStream; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; - -/** - * Caches and loads FeedImage bitmaps in the background - */ -public class ImageLoader { - private static final String TAG = "ImageLoader"; - private static ImageLoader singleton; - - public static final int IMAGE_TYPE_THUMBNAIL = 0; - public static final int IMAGE_TYPE_COVER = 1; - - /** - * Used by loadThumbnailBitmap and loadCoverBitmap to denote an ImageView that displays the default image resource. - * This is the case if the given source to load the image from was null or did not return any image data. - */ - private static final Object DEFAULT_IMAGE_RESOURCE_TAG = new Object(); - - private Handler handler; - private ExecutorService executor; - - /** - * Stores references to loaded bitmaps. Bitmaps can be accessed by the id of - * the FeedImage the bitmap belongs to. - */ - - final int memClass = ((ActivityManager) PodcastApp.getInstance() - .getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); - - // Use 1/8th of the available memory for this memory cache. - final int thumbnailCacheSize = 1024 * 1024 * memClass / 8; - - private LruCache<String, CachedBitmap> coverCache; - private LruCache<String, CachedBitmap> thumbnailCache; - - private ImageLoader() { - handler = new Handler(); - executor = createExecutor(); - - coverCache = new LruCache<String, CachedBitmap>(1); - - thumbnailCache = new LruCache<String, CachedBitmap>(thumbnailCacheSize) { - - @SuppressLint("NewApi") - @Override - protected int sizeOf(String key, CachedBitmap value) { - if (Integer.valueOf(android.os.Build.VERSION.SDK_INT) >= 12) - return value.getBitmap().getByteCount(); - else - return (value.getBitmap().getRowBytes() * value.getBitmap() - .getHeight()); - - } - - }; - } - - private ExecutorService createExecutor() { - return Executors.newFixedThreadPool(Runtime.getRuntime() - .availableProcessors(), new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - } - - public static synchronized ImageLoader getInstance() { - if (singleton == null) { - singleton = new ImageLoader(); - } - return singleton; - } - - /** - * Load a bitmap from the cover cache. If the bitmap is not in the cache, it - * will be loaded from the disk. This method should either be called if the - * ImageView's size has already been set or inside a Runnable which is - * posted to the ImageView's message queue. - */ - public void loadCoverBitmap(ImageWorkerTaskResource source, ImageView target) { - loadCoverBitmap(source, target, target.getHeight()); - } - - /** - * Load a bitmap from the cover cache. If the bitmap is not in the cache, it - * will be loaded from the disk. This method should either be called if the - * ImageView's size has already been set or inside a Runnable which is - * posted to the ImageView's message queue. - */ - public void loadCoverBitmap(ImageWorkerTaskResource source, - ImageView target, int length) { - final int defaultCoverResource = getDefaultCoverResource(target - .getContext()); - final String cacheKey; - if (source != null && (cacheKey = source.getImageLoaderCacheKey()) != null) { - final Object currentTag = target.getTag(R.id.imageloader_key); - if (currentTag == null || !cacheKey.equals(currentTag)) { - target.setTag(R.id.imageloader_key, cacheKey); - CachedBitmap cBitmap = getBitmapFromCoverCache(cacheKey); - if (cBitmap != null && cBitmap.getLength() >= length) { - target.setImageBitmap(cBitmap.getBitmap()); - } else { - target.setImageResource(defaultCoverResource); - BitmapDecodeWorkerTask worker = new BitmapDecodeWorkerTask( - handler, target, source, length, IMAGE_TYPE_COVER); - executor.submit(worker); - } - } - } else { - target.setImageResource(defaultCoverResource); - target.setTag(R.id.imageloader_key, DEFAULT_IMAGE_RESOURCE_TAG); - } - } - - /** - * Load a bitmap from the thumbnail cache. If the bitmap is not in the - * cache, it will be loaded from the disk. This method should either be - * called if the ImageView's size has already been set or inside a Runnable - * which is posted to the ImageView's message queue. - */ - public void loadThumbnailBitmap(ImageWorkerTaskResource source, - ImageView target) { - loadThumbnailBitmap(source, target, target.getHeight()); - } - - /** - * Load a bitmap from the thumbnail cache. If the bitmap is not in the - * cache, it will be loaded from the disk. This method should either be - * called if the ImageView's size has already been set or inside a Runnable - * which is posted to the ImageView's message queue. - */ - public void loadThumbnailBitmap(ImageWorkerTaskResource source, - ImageView target, int length) { - final int defaultCoverResource = getDefaultCoverResource(target - .getContext()); - final String cacheKey; - if (source != null && (cacheKey = source.getImageLoaderCacheKey()) != null) { - final Object currentTag = target.getTag(R.id.imageloader_key); - if (currentTag == null || !cacheKey.equals(currentTag)) { - target.setTag(R.id.imageloader_key, cacheKey); - CachedBitmap cBitmap = getBitmapFromThumbnailCache(cacheKey); - if (cBitmap != null && cBitmap.getLength() >= length) { - target.setImageBitmap(cBitmap.getBitmap()); - } else { - target.setImageResource(defaultCoverResource); - BitmapDecodeWorkerTask worker = new BitmapDecodeWorkerTask( - handler, target, source, length, IMAGE_TYPE_THUMBNAIL); - executor.submit(worker); - } - } - } else { - target.setImageResource(defaultCoverResource); - target.setTag(R.id.imageloader_key, DEFAULT_IMAGE_RESOURCE_TAG); - } - } - - public void clearExecutorQueue() { - executor.shutdownNow(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Executor was shut down."); - executor = createExecutor(); - - } - - public void wipeImageCache() { - coverCache.evictAll(); - thumbnailCache.evictAll(); - } - - public boolean isInThumbnailCache(String fileUrl) { - return thumbnailCache.get(fileUrl) != null; - } - - private CachedBitmap getBitmapFromThumbnailCache(String key) { - return thumbnailCache.get(key); - } - - public void addBitmapToThumbnailCache(String key, CachedBitmap bitmap) { - thumbnailCache.put(key, bitmap); - } - - public boolean isInCoverCache(String fileUrl) { - return coverCache.get(fileUrl) != null; - } - - private CachedBitmap getBitmapFromCoverCache(String key) { - return coverCache.get(key); - } - - public void addBitmapToCoverCache(String key, CachedBitmap bitmap) { - coverCache.put(key, bitmap); - } - - private int getDefaultCoverResource(Context context) { - return android.R.color.transparent; - } - - /** - * Used by the BitmapDecodeWorker task to retrieve the source of the bitmap. - */ - public interface ImageWorkerTaskResource { - /** - * Opens a new InputStream that can be decoded as a bitmap by the - * BitmapFactory. - */ - public InputStream openImageInputStream(); - - /** - * Returns an InputStream that points to the beginning of the image - * resource. Implementations can either create a new InputStream or - * reset the existing one, depending on their implementation of - * openInputStream. If a new InputStream is returned, the one given as a - * parameter MUST be closed. - * - * @param input The input stream that was returned by openImageInputStream() - */ - public InputStream reopenImageInputStream(InputStream input); - - /** - * Returns a string that identifies the image resource. Example: file - * path of an image - */ - public String getImageLoaderCacheKey(); - } - -} diff --git a/src/de/danoeh/antennapod/asynctask/PicassoImageResource.java b/src/de/danoeh/antennapod/asynctask/PicassoImageResource.java new file mode 100644 index 000000000..26f9d9278 --- /dev/null +++ b/src/de/danoeh/antennapod/asynctask/PicassoImageResource.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.asynctask; + +import android.net.Uri; + +/** + * Classes that implement this interface provide access to an image resource that can + * be loaded by the Picasso library. + */ +public interface PicassoImageResource { + + /** + * This scheme should be used by PicassoImageResources to + * indicate that the image Uri points to a file that is not an image + * (e.g. a media file). This workaround is needed so that the Picasso library + * loads these Uri with a Downloader instead of trying to load it directly. + * <p/> + * For example implementations, see FeedMedia or ExternalMedia. + */ + public static final String SCHEME_MEDIA = "media"; + + + /** + * Parameter key for an encoded fallback Uri. This Uri MUST point to a local image file + */ + public static final String PARAM_FALLBACK = "fallback"; + + /** + * Returns a Uri to the image or null if no image is available. + * <p/> + * The Uri can either be an HTTP-URL, a URL pointing to a local image file or + * a non-image file (see SCHEME_MEDIA for more details). + * <p/> + * The Uri can also have an optional fallback-URL if loading the default URL + * failed (see PARAM_FALLBACK). + */ + public Uri getImageUri(); +} diff --git a/src/de/danoeh/antennapod/asynctask/PicassoProvider.java b/src/de/danoeh/antennapod/asynctask/PicassoProvider.java new file mode 100644 index 000000000..849725630 --- /dev/null +++ b/src/de/danoeh/antennapod/asynctask/PicassoProvider.java @@ -0,0 +1,152 @@ +package de.danoeh.antennapod.asynctask; + +import android.content.Context; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import com.squareup.picasso.Cache; +import com.squareup.picasso.Downloader; +import com.squareup.picasso.LruCache; +import com.squareup.picasso.OkHttpDownloader; +import com.squareup.picasso.Picasso; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Provides access to Picasso instances. + */ +public class PicassoProvider { + private static final String TAG = "PicassoProvider"; + + private static final boolean DEBUG = false; + + private static ExecutorService executorService; + private static Cache memoryCache; + + private static Picasso defaultPicassoInstance; + private static Picasso mediaMetadataPicassoInstance; + + private static synchronized ExecutorService getExecutorService() { + if (executorService == null) { + executorService = Executors.newFixedThreadPool(3); + } + return executorService; + } + + private static synchronized Cache getMemoryCache(Context context) { + if (memoryCache == null) { + memoryCache = new LruCache(context); + } + return memoryCache; + } + + /** + * Returns a Picasso instance that uses an OkHttpDownloader. This instance can only load images + * from image files. + * <p/> + * This instance should be used as long as no images from media files are loaded. + */ + public static synchronized Picasso getDefaultPicassoInstance(Context context) { + Validate.notNull(context); + if (defaultPicassoInstance == null) { + defaultPicassoInstance = new Picasso.Builder(context) + .indicatorsEnabled(DEBUG) + .loggingEnabled(DEBUG) + .downloader(new OkHttpDownloader(context)) + .executor(getExecutorService()) + .memoryCache(getMemoryCache(context)) + .listener(new Picasso.Listener() { + @Override + public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) { + Log.e(TAG, "Failed to load Uri:" + uri.toString()); + e.printStackTrace(); + } + }) + .build(); + } + return defaultPicassoInstance; + } + + /** + * Returns a Picasso instance that uses a MediaMetadataRetriever if the given Uri is a media file + * and a default OkHttpDownloader otherwise. + */ + public static synchronized Picasso getMediaMetadataPicassoInstance(Context context) { + Validate.notNull(context); + if (mediaMetadataPicassoInstance == null) { + mediaMetadataPicassoInstance = new Picasso.Builder(context) + .indicatorsEnabled(DEBUG) + .loggingEnabled(DEBUG) + .downloader(new MediaMetadataDownloader(context)) + .executor(getExecutorService()) + .memoryCache(getMemoryCache(context)) + .listener(new Picasso.Listener() { + @Override + public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) { + Log.e(TAG, "Failed to load Uri:" + uri.toString()); + e.printStackTrace(); + } + }) + .build(); + } + return mediaMetadataPicassoInstance; + } + + private static class MediaMetadataDownloader implements Downloader { + + private static final String TAG = "MediaMetadataDownloader"; + + private final OkHttpDownloader okHttpDownloader; + + public MediaMetadataDownloader(Context context) { + Validate.notNull(context); + okHttpDownloader = new OkHttpDownloader(context); + } + + @Override + public Response load(Uri uri, boolean b) throws IOException { + if (StringUtils.equals(uri.getScheme(), PicassoImageResource.SCHEME_MEDIA)) { + String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(uri.getLastPathSegment())); + if (StringUtils.startsWith(type, "image")) { + File imageFile = new File(uri.toString()); + return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length()); + } else { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(uri.getPath()); + byte[] data = mmr.getEmbeddedPicture(); + mmr.release(); + + if (data != null) { + return new Response(new ByteArrayInputStream(data), true, data.length); + } else { + + // check for fallback Uri + String fallbackParam = uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK); + + if (fallbackParam != null) { + String fallback = Uri.decode(Uri.parse(fallbackParam).getPath()); + if (fallback != null) { + File imageFile = new File(fallback); + return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length()); + } + } + return null; + } + } + } + return okHttpDownloader.load(uri, b); + } + } +} diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java index f9da65e03..b5415c69c 100644 --- a/src/de/danoeh/antennapod/feed/Feed.java +++ b/src/de/danoeh/antennapod/feed/Feed.java @@ -1,6 +1,9 @@ package de.danoeh.antennapod.feed; import android.content.Context; +import android.net.Uri; + +import de.danoeh.antennapod.asynctask.PicassoImageResource; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.EpisodeFilter; @@ -16,7 +19,7 @@ import java.util.List; * * @author daniel */ -public class Feed extends FeedFile implements FlattrThing { +public class Feed extends FeedFile implements FlattrThing, PicassoImageResource { public static final int FEEDFILETYPE_FEED = 0; public static final String TYPE_RSS2 = "rss"; public static final String TYPE_RSS091 = "rss"; @@ -430,4 +433,13 @@ public class Feed extends FeedFile implements FlattrThing { preferences.setFeedID(id); } } + + @Override + public Uri getImageUri() { + if (image != null) { + return image.getImageUri(); + } else { + return null; + } + } } diff --git a/src/de/danoeh/antennapod/feed/FeedComponent.java b/src/de/danoeh/antennapod/feed/FeedComponent.java index 66a2f9cc5..48b243770 100644 --- a/src/de/danoeh/antennapod/feed/FeedComponent.java +++ b/src/de/danoeh/antennapod/feed/FeedComponent.java @@ -2,43 +2,43 @@ package de.danoeh.antennapod.feed; /** * Represents every possible component of a feed - * @author daniel * + * @author daniel */ public abstract class FeedComponent { - protected long id; - - public FeedComponent() { - super(); - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - /** - * Update this FeedComponent's attributes with the attributes from another - * FeedComponent. This method should only update attributes which where read from - * the feed. - */ - public void updateFromOther(FeedComponent other) { - } - - /** - * Compare's this FeedComponent's attribute values with another FeedComponent's - * attribute values. This method will only compare attributes which were - * read from the feed. - * - * @return true if attribute values are different, false otherwise - */ - public boolean compareWithOther(FeedComponent other) { - return false; - } + protected long id; + + public FeedComponent() { + super(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + /** + * Update this FeedComponent's attributes with the attributes from another + * FeedComponent. This method should only update attributes which where read from + * the feed. + */ + public void updateFromOther(FeedComponent other) { + } + + /** + * Compare's this FeedComponent's attribute values with another FeedComponent's + * attribute values. This method will only compare attributes which were + * read from the feed. + * + * @return true if attribute values are different, false otherwise + */ + public boolean compareWithOther(FeedComponent other) { + return false; + } /** @@ -47,4 +47,20 @@ public abstract class FeedComponent { */ public abstract String getHumanReadableIdentifier(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FeedComponent that = (FeedComponent) o; + + if (id != that.id) return false; + + return true; + } + + @Override + public int hashCode() { + return (int) (id ^ (id >>> 32)); + } }
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/feed/FeedImage.java b/src/de/danoeh/antennapod/feed/FeedImage.java index 9c9170294..c588f5e71 100644 --- a/src/de/danoeh/antennapod/feed/FeedImage.java +++ b/src/de/danoeh/antennapod/feed/FeedImage.java @@ -1,6 +1,9 @@ package de.danoeh.antennapod.feed; -import de.danoeh.antennapod.asynctask.ImageLoader; +import android.net.Uri; + +import de.danoeh.antennapod.asynctask.PicassoImageResource; + import org.apache.commons.io.IOUtils; import java.io.File; @@ -10,8 +13,7 @@ import java.io.InputStream; -public class FeedImage extends FeedFile implements - ImageLoader.ImageWorkerTaskResource { +public class FeedImage extends FeedFile implements PicassoImageResource { public static final int FEEDFILETYPE_FEEDIMAGE = 1; protected String title; @@ -64,30 +66,12 @@ public class FeedImage extends FeedFile implements this.owner = owner; } - @Override - public InputStream openImageInputStream() { - if (file_url != null) { - File file = new File(file_url); - if (file.exists()) { - try { - return new FileInputStream(file_url); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - } - } - return null; - } - - @Override - public String getImageLoaderCacheKey() { - return file_url; - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - IOUtils.closeQuietly(input); - return openImageInputStream(); - } - + @Override + public Uri getImageUri() { + if (file_url != null && downloaded) { + return Uri.fromFile(new File(file_url)); + } else { + return null; + } + } } diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java index 956131ab2..78091ea33 100644 --- a/src/de/danoeh/antennapod/feed/FeedItem.java +++ b/src/de/danoeh/antennapod/feed/FeedItem.java @@ -1,7 +1,9 @@ package de.danoeh.antennapod.feed; +import android.net.Uri; + import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoImageResource; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.util.ShownotesProvider; import de.danoeh.antennapod.util.flattr.FlattrStatus; @@ -17,8 +19,7 @@ import java.util.concurrent.Callable; * * @author daniel */ -public class FeedItem extends FeedComponent implements - ImageLoader.ImageWorkerTaskResource, ShownotesProvider, FlattrThing { +public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource { /** * The id/guid that can be found in the rss/atom feed. Might not be set. @@ -261,6 +262,17 @@ public class FeedItem extends FeedComponent implements }; } + @Override + public Uri getImageUri() { + if (hasMedia()) { + return media.getImageUri(); + } else if (feed != null) { + return feed.getImageUri(); + } else { + return null; + } + } + public enum State { NEW, IN_PROGRESS, READ, PLAYING } @@ -277,45 +289,6 @@ public class FeedItem extends FeedComponent implements return (isRead() ? State.READ : State.NEW); } - @Override - public InputStream openImageInputStream() { - InputStream out = null; - if (hasItemImageDownloaded()) { - out = image.openImageInputStream(); - } else if (hasMedia()) { - out = media.openImageInputStream(); - } else if (feed.getImage() != null) { - out = feed.getImage().openImageInputStream(); - } - return out; - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - InputStream out = null; - if (hasItemImageDownloaded()) { - out = image.reopenImageInputStream(input); - } else if (hasMedia()) { - out = media.reopenImageInputStream(input); - } else if (feed.getImage() != null) { - out = feed.getImage().reopenImageInputStream(input); - } - return out; - } - - @Override - public String getImageLoaderCacheKey() { - String out = null; - if (hasItemImageDownloaded()) { - out = image.getImageLoaderCacheKey(); - } else if (hasMedia()) { - out = media.getImageLoaderCacheKey(); - } else if (feed.getImage() != null) { - out = feed.getImage().getImageLoaderCacheKey(); - } - return out; - } - public long getFeedId() { return feedId; } diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java index dc941cb48..9298ebe8a 100644 --- a/src/de/danoeh/antennapod/feed/FeedMedia.java +++ b/src/de/danoeh/antennapod/feed/FeedMedia.java @@ -2,21 +2,24 @@ package de.danoeh.antennapod.feed; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.ChapterUtils; -import de.danoeh.antennapod.util.playback.Playable; +import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.preferences.PlaybackPreferences; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.ChapterUtils; +import de.danoeh.antennapod.util.playback.Playable; + public class FeedMedia extends FeedFile implements Playable { private static final String TAG = "FeedMedia"; @@ -382,52 +385,27 @@ public class FeedMedia extends FeedFile implements Playable { }; @Override - public InputStream openImageInputStream() { - InputStream out; - if (item.hasItemImageDownloaded()) { - out = item.openImageInputStream(); - } else { - out = new Playable.DefaultPlayableImageLoader(this) - .openImageInputStream(); - } - if (out == null) { - if (item.getFeed().getImage() != null) { - return item.getFeed().getImage().openImageInputStream(); + public Uri getImageUri() { + final Uri feedImgUri = getFeedImageUri(); + + if (localFileAvailable()) { + Uri.Builder builder = new Uri.Builder(); + builder.scheme(SCHEME_MEDIA) + .encodedPath(getLocalMediaUrl()); + if (feedImgUri != null) { + builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString()); } - } - return out; - } - - @Override - public String getImageLoaderCacheKey() { - String out; - if (item == null) { - return null; - } else if (item.hasItemImageDownloaded()) { - out = item.getImageLoaderCacheKey(); + return builder.build(); } else { - out = new Playable.DefaultPlayableImageLoader(this) - .getImageLoaderCacheKey(); - } - if (out == null) { - if (item.getFeed().getImage() != null) { - return item.getFeed().getImage().getImageLoaderCacheKey(); - } + return feedImgUri; } - return out; } - @Override - public InputStream reopenImageInputStream(InputStream input) { - if (input instanceof FileInputStream) { - if (item.hasItemImageDownloaded()) { - return item.getImage().reopenImageInputStream(input); - } else { - return item.getFeed().getImage().reopenImageInputStream(input); - } + private Uri getFeedImageUri() { + if (item != null && item.getFeed() != null) { + return item.getFeed().getImageUri(); } else { - return new Playable.DefaultPlayableImageLoader(this) - .reopenImageInputStream(input); + return null; } } } diff --git a/src/de/danoeh/antennapod/feed/MP4Chapter.java b/src/de/danoeh/antennapod/feed/MP4Chapter.java new file mode 100644 index 000000000..a5e1df393 --- /dev/null +++ b/src/de/danoeh/antennapod/feed/MP4Chapter.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.feed; + +import wseemann.media.FFmpegChapter; + +/** + * Represents a chapter contained in a MP4 file. + */ +public class MP4Chapter extends Chapter { + public static final int CHAPTERTYPE_MP4CHAPTER = 4; + + /** + * Construct a MP4Chapter from an FFmpegChapter. + */ + public MP4Chapter(FFmpegChapter ch) { + this.start = ch.getStart(); + this.title = ch.getTitle(); + } + + public MP4Chapter(long start, String title, FeedItem item, String link) { + super(start, title, item, link); + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_MP4CHAPTER; + } +} diff --git a/src/de/danoeh/antennapod/fragment/CoverFragment.java b/src/de/danoeh/antennapod/fragment/CoverFragment.java index 0e1fe35e0..ffce518bf 100644 --- a/src/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/src/de/danoeh/antennapod/fragment/CoverFragment.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.fragment; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.Log; @@ -7,91 +8,98 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.util.playback.Playable; -/** Displays the cover and the title of a FeedItem. */ +/** + * Displays the cover and the title of a FeedItem. + */ public class CoverFragment extends Fragment implements - AudioplayerContentFragment { - private static final String TAG = "CoverFragment"; - private static final String ARG_PLAYABLE = "arg.playable"; + AudioplayerContentFragment { + private static final String TAG = "CoverFragment"; + private static final String ARG_PLAYABLE = "arg.playable"; - private Playable media; + private Playable media; - private ImageView imgvCover; + private ImageView imgvCover; - private boolean viewCreated = false; + private boolean viewCreated = false; - public static CoverFragment newInstance(Playable item) { - CoverFragment f = new CoverFragment(); - if (item != null) { - Bundle args = new Bundle(); - args.putParcelable(ARG_PLAYABLE, item); - f.setArguments(args); - } - return f; - } + public static CoverFragment newInstance(Playable item) { + CoverFragment f = new CoverFragment(); + if (item != null) { + Bundle args = new Bundle(); + args.putParcelable(ARG_PLAYABLE, item); + f.setArguments(args); + } + return f; + } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - Bundle args = getArguments(); - if (args != null) { - media = args.getParcelable(ARG_PLAYABLE); - } else { - Log.e(TAG, TAG + " was called with invalid arguments"); - } - } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + Bundle args = getArguments(); + if (args != null) { + media = args.getParcelable(ARG_PLAYABLE); + } else { + Log.e(TAG, TAG + " was called with invalid arguments"); + } + } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.cover_fragment, container, false); - imgvCover = (ImageView) root.findViewById(R.id.imgvCover); - viewCreated = true; - return root; - } + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.cover_fragment, container, false); + imgvCover = (ImageView) root.findViewById(R.id.imgvCover); + viewCreated = true; + return root; + } - private void loadMediaInfo() { - if (media != null) { - imgvCover.post(new Runnable() { + private void loadMediaInfo() { + if (media != null) { + imgvCover.post(new Runnable() { - @Override - public void run() { - ImageLoader.getInstance().loadCoverBitmap( - media, imgvCover); - } - }); - } else { - Log.w(TAG, "loadMediaInfo was called while media was null"); - } - } + @Override + public void run() { + Context c = getActivity(); + if (c != null) { + PicassoProvider.getMediaMetadataPicassoInstance(c) + .load(media.getImageUri()) + .into(imgvCover); + } + } + }); + } else { + Log.w(TAG, "loadMediaInfo was called while media was null"); + } + } - @Override - public void onStart() { - if (BuildConfig.DEBUG) - Log.d(TAG, "On Start"); - super.onStart(); - if (media != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading media info"); - loadMediaInfo(); - } else { - Log.w(TAG, "Unable to load media info: media was null"); - } - } + @Override + public void onStart() { + if (BuildConfig.DEBUG) + Log.d(TAG, "On Start"); + super.onStart(); + if (media != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading media info"); + loadMediaInfo(); + } else { + Log.w(TAG, "Unable to load media info: media was null"); + } + } - @Override - public void onDataSetChanged(Playable media) { - this.media = media; - if (viewCreated) { - loadMediaInfo(); - } + @Override + public void onDataSetChanged(Playable media) { + this.media = media; + if (viewCreated) { + loadMediaInfo(); + } - } + } } diff --git a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index db47cd8a4..985673dd3 100644 --- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -10,9 +10,10 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.playback.Playable; @@ -23,215 +24,215 @@ import de.danoeh.antennapod.util.playback.PlaybackController; * if the PlaybackService is running */ public class ExternalPlayerFragment extends Fragment { - private static final String TAG = "ExternalPlayerFragment"; - - private ViewGroup fragmentLayout; - private ImageView imgvCover; - private ViewGroup layoutInfo; - private TextView txtvTitle; - private ImageButton butPlay; - - private PlaybackController controller; - - public ExternalPlayerFragment() { - super(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.external_player_fragment, - container, false); - fragmentLayout = (ViewGroup) root.findViewById(R.id.fragmentLayout); - imgvCover = (ImageView) root.findViewById(R.id.imgvCover); - layoutInfo = (ViewGroup) root.findViewById(R.id.layoutInfo); - txtvTitle = (TextView) root.findViewById(R.id.txtvTitle); - butPlay = (ImageButton) root.findViewById(R.id.butPlay); - - layoutInfo.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - if (BuildConfig.DEBUG) - Log.d(TAG, "layoutInfo was clicked"); - - if (controller.getMedia() != null) { - startActivity(PlaybackService.getPlayerActivityIntent( - getActivity(), controller.getMedia())); - } - } - }); - return root; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - controller = setupPlaybackController(); - butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); - } - - private PlaybackController setupPlaybackController() { - return new PlaybackController(getActivity(), true) { - - @Override - public void setupGUI() { - } - - @Override - public void onPositionObserverUpdate() { - } - - @Override - public void onReloadNotification(int code) { - } - - @Override - public void onBufferStart() { - // TODO Auto-generated method stub - - } - - @Override - public void onBufferEnd() { - // TODO Auto-generated method stub - - } - - @Override - public void onBufferUpdate(float progress) { - } - - @Override - public void onSleepTimerUpdate() { - } - - @Override - public void handleError(int code) { - } - - @Override - public ImageButton getPlayButton() { - return butPlay; - } - - @Override - public void postStatusMsg(int msg) { - } - - @Override - public void clearStatusMsg() { - } - - @Override - public boolean loadMediaInfo() { + private static final String TAG = "ExternalPlayerFragment"; + + private ViewGroup fragmentLayout; + private ImageView imgvCover; + private ViewGroup layoutInfo; + private TextView txtvTitle; + private ImageButton butPlay; + + private PlaybackController controller; + + public ExternalPlayerFragment() { + super(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.external_player_fragment, + container, false); + fragmentLayout = (ViewGroup) root.findViewById(R.id.fragmentLayout); + imgvCover = (ImageView) root.findViewById(R.id.imgvCover); + layoutInfo = (ViewGroup) root.findViewById(R.id.layoutInfo); + txtvTitle = (TextView) root.findViewById(R.id.txtvTitle); + butPlay = (ImageButton) root.findViewById(R.id.butPlay); + + layoutInfo.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (BuildConfig.DEBUG) + Log.d(TAG, "layoutInfo was clicked"); + + if (controller.getMedia() != null) { + startActivity(PlaybackService.getPlayerActivityIntent( + getActivity(), controller.getMedia())); + } + } + }); + return root; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + controller = setupPlaybackController(); + butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); + } + + private PlaybackController setupPlaybackController() { + return new PlaybackController(getActivity(), true) { + + @Override + public void setupGUI() { + } + + @Override + public void onPositionObserverUpdate() { + } + + @Override + public void onReloadNotification(int code) { + } + + @Override + public void onBufferStart() { + // TODO Auto-generated method stub + + } + + @Override + public void onBufferEnd() { + // TODO Auto-generated method stub + + } + + @Override + public void onBufferUpdate(float progress) { + } + + @Override + public void onSleepTimerUpdate() { + } + + @Override + public void handleError(int code) { + } + + @Override + public ImageButton getPlayButton() { + return butPlay; + } + + @Override + public void postStatusMsg(int msg) { + } + + @Override + public void clearStatusMsg() { + } + + @Override + public boolean loadMediaInfo() { ExternalPlayerFragment fragment = ExternalPlayerFragment.this; if (fragment != null) { - return fragment.loadMediaInfo(); + return fragment.loadMediaInfo(); } else { return false; } - } - - @Override - public void onAwaitingVideoSurface() { - } - - @Override - public void onServiceQueried() { - } - - @Override - public void onShutdownNotification() { - if (fragmentLayout != null) { - fragmentLayout.setVisibility(View.GONE); - } - controller = setupPlaybackController(); - if (butPlay != null) { - butPlay.setOnClickListener(controller - .newOnPlayButtonClickListener()); - } - - } - - @Override - public void onPlaybackEnd() { - if (fragmentLayout != null) { - fragmentLayout.setVisibility(View.GONE); - } - controller = setupPlaybackController(); - if (butPlay != null) { - butPlay.setOnClickListener(controller - .newOnPlayButtonClickListener()); - } - } - - @Override - public void onPlaybackSpeedChange() { - // TODO Auto-generated method stub - - } - }; - } - - @Override - public void onResume() { - super.onResume(); - controller.init(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Fragment is about to be destroyed"); - if (controller != null) { - controller.release(); - } - } - - @Override - public void onPause() { - super.onPause(); - if (controller != null) { - controller.pause(); - } - } - - private boolean loadMediaInfo() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading media info"); - if (controller.serviceAvailable()) { - Playable media = controller.getMedia(); - if (media != null) { - txtvTitle.setText(media.getEpisodeTitle()); - ImageLoader.getInstance().loadThumbnailBitmap( - media, - imgvCover, - (int) getActivity().getResources().getDimension( - R.dimen.external_player_height)); - - fragmentLayout.setVisibility(View.VISIBLE); - if (controller.isPlayingVideo()) { - butPlay.setVisibility(View.GONE); - } else { - butPlay.setVisibility(View.VISIBLE); - } + } + + @Override + public void onAwaitingVideoSurface() { + } + + @Override + public void onServiceQueried() { + } + + @Override + public void onShutdownNotification() { + if (fragmentLayout != null) { + fragmentLayout.setVisibility(View.GONE); + } + controller = setupPlaybackController(); + if (butPlay != null) { + butPlay.setOnClickListener(controller + .newOnPlayButtonClickListener()); + } + + } + + @Override + public void onPlaybackEnd() { + if (fragmentLayout != null) { + fragmentLayout.setVisibility(View.GONE); + } + controller = setupPlaybackController(); + if (butPlay != null) { + butPlay.setOnClickListener(controller + .newOnPlayButtonClickListener()); + } + } + + @Override + public void onPlaybackSpeedChange() { + // TODO Auto-generated method stub + + } + }; + } + + @Override + public void onResume() { + super.onResume(); + controller.init(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Fragment is about to be destroyed"); + if (controller != null) { + controller.release(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (controller != null) { + controller.pause(); + } + } + + private boolean loadMediaInfo() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading media info"); + if (controller.serviceAvailable()) { + Playable media = controller.getMedia(); + if (media != null) { + txtvTitle.setText(media.getEpisodeTitle()); + + PicassoProvider.getMediaMetadataPicassoInstance(getActivity()) + .load(media.getImageUri()) + .fit() + .into(imgvCover); + + fragmentLayout.setVisibility(View.VISIBLE); + if (controller.isPlayingVideo()) { + butPlay.setVisibility(View.GONE); + } else { + butPlay.setVisibility(View.VISIBLE); + } return true; - } else { - Log.w(TAG, - "loadMediaInfo was called while the media object of playbackService was null!"); + } 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!"); + } + } else { + Log.w(TAG, + "loadMediaInfo was called while playbackService was null!"); return false; - } - } + } + } - private String getPositionString(int position, int duration) { - return Converter.getDurationStringLong(position) + " / " - + Converter.getDurationStringLong(duration); - } + private String getPositionString(int position, int duration) { + return Converter.getDurationStringLong(position) + " / " + + Converter.getDurationStringLong(duration); + } } diff --git a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java index d37f17b6d..909774467 100644 --- a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -34,7 +34,7 @@ import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.adapter.FeedItemlistAdapter; import de.danoeh.antennapod.asynctask.DownloadObserver; import de.danoeh.antennapod.asynctask.FeedRemover; -import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.asynctask.PicassoProvider; import de.danoeh.antennapod.dialog.ConfirmationDialog; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.dialog.FeedItemDialog; @@ -349,8 +349,12 @@ public class ItemlistFragment extends ListFragment { txtvTitle.setText(feed.getTitle()); txtvAuthor.setText(feed.getAuthor()); - ImageLoader.getInstance().loadThumbnailBitmap(feed.getImage(), imgvCover, - (int) getResources().getDimension(R.dimen.thumbnail_length_onlinefeedview)); + + PicassoProvider.getDefaultPicassoInstance(getActivity()) + .load(feed.getImageUri()) + .fit() + .into(imgvCover); + if (feed.getLink() == null) { butVisitWebsite.setVisibility(View.INVISIBLE); } else { diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 2289862aa..a7e1033df 100644 --- a/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.fragment.gpodnet; +import android.app.Activity; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; @@ -43,8 +44,11 @@ public class TagListFragment extends ListFragment { sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s)); + Activity activity = getActivity(); + if (activity != null) { + sv.clearFocus(); + ((MainActivity) activity).loadChildFragment(SearchListFragment.newInstance(s)); + } return true; } diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java index 61a0562e6..d4f66b870 100644 --- a/src/de/danoeh/antennapod/service/playback/PlaybackService.java +++ b/src/de/danoeh/antennapod/service/playback/PlaybackService.java @@ -4,7 +4,12 @@ import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; -import android.content.*; +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; @@ -26,11 +31,14 @@ import android.widget.Toast; import org.apache.commons.lang3.StringUtils; +import java.io.IOException; +import java.util.List; + import de.danoeh.antennapod.BuildConfig; -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.asynctask.PicassoProvider; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; @@ -41,14 +49,10 @@ 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.FlattrThing; import de.danoeh.antennapod.util.flattr.FlattrUtils; import de.danoeh.antennapod.util.playback.Playable; -import java.util.List; - /** * Controls the MediaPlayer that plays a FeedMedia-file */ @@ -257,7 +261,8 @@ public class PlaybackService extends Service { } if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - if (BuildConfig.DEBUG) Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + if (BuildConfig.DEBUG) + Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); stopForeground(true); } else { @@ -700,11 +705,16 @@ public class PlaybackService extends Service { 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); + try { + int iconSize = getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + icon = PicassoProvider.getMediaMetadataPicassoInstance(PlaybackService.this) + .load(info.playable.getImageUri()) + .resize(iconSize, iconSize) + .get(); + } catch (IOException e) { + e.printStackTrace(); + } } } @@ -813,7 +823,7 @@ public class PlaybackService extends Service { if (updatePlayedDuration && playable instanceof FeedMedia) { FeedMedia m = (FeedMedia) playable; FeedItem item = m.getItem(); - m.setPlayedDuration(m.getPlayedDuration() + ((int)(deltaPlayedDuration * playbackSpeed))); + m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); // Auto flattr if (isAutoFlattrable(m) && (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { @@ -825,8 +835,9 @@ public class PlaybackService extends Service { } } playable.saveCurrentPosition(PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()), - position); + .getDefaultSharedPreferences(getApplicationContext()), + position + ); } } diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java index 477eea9a6..49f20012d 100644 --- a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java +++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java @@ -4,12 +4,23 @@ import android.content.ComponentName; import android.content.Context; import android.media.AudioManager; import android.media.RemoteControlClient; +import android.net.wifi.WifiManager; +import android.os.PowerManager; +import android.telephony.TelephonyManager; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; import org.apache.commons.lang3.Validate; +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; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.MediaType; @@ -20,14 +31,6 @@ 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. */ @@ -63,6 +66,11 @@ public class PlaybackServiceMediaPlayer { private final ThreadPoolExecutor executor; + /** + * A wifi-lock that is acquired if the media file is being streamed. + */ + private WifiManager.WifiLock wifiLock; + public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { Validate.notNull(context); Validate.notNull(callback); @@ -227,7 +235,7 @@ public class PlaybackServiceMediaPlayer { audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - + acquireWifiLockIfNecessary(); setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); mediaPlayer.start(); if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { @@ -273,7 +281,7 @@ public class PlaybackServiceMediaPlayer { @Override public void run() { playerLock.lock(); - + releaseWifiLockIfNecessary(); if (playerStatus == PlayerStatus.PLAYING) { if (BuildConfig.DEBUG) Log.d(TAG, "Pausing playback."); @@ -374,13 +382,14 @@ public class PlaybackServiceMediaPlayer { @Override public void run() { playerLock.lock(); - + releaseWifiLockIfNecessary(); if (media != null) { playMediaObject(media, true, stream, startWhenPrepared.get(), false); } else if (mediaPlayer != null) { mediaPlayer.reset(); } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + if (BuildConfig.DEBUG) + Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); } playerLock.unlock(); } @@ -590,6 +599,7 @@ public class PlaybackServiceMediaPlayer { if (mediaPlayer != null) { mediaPlayer.release(); } + releaseWifiLockIfNecessary(); } public void setVideoSurface(final SurfaceHolder surface) { @@ -682,6 +692,7 @@ public class PlaybackServiceMediaPlayer { mediaPlayer = new AudioPlayer(context); } mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); return setMediaPlayerListeners(mediaPlayer); } @@ -694,59 +705,62 @@ public class PlaybackServiceMediaPlayer { public void run() { playerLock.lock(); - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); - pause(true, false); - callback.shouldStop(); - break; - case AudioManager.AUDIOFOCUS_GAIN: - if (BuildConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); - if (pausedBecauseOfTransientAudiofocusLoss) // we paused => play now - resume(); - else // we ducked => raise audio level back + // If there is an incoming call, playback should be paused permanently + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final int callState = (tm != null) ? tm.getCallState() : 0; + if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState); + Log.i(TAG, "Call state:" + callState); + + if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus"); + pause(true, false); + callback.shouldStop(); + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + if (BuildConfig.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); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + if (playerStatus == PlayerStatus.PLAYING) { + if (!UserPreferences.shouldPauseForFocusLoss()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); 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 (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, 0); - pausedBecauseOfTransientAudiofocusLoss = false; - } else { - if (BuildConfig.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) { + AudioManager.ADJUST_LOWER, 0); + pausedBecauseOfTransientAudiofocusLoss = false; + } else { if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + if (playerStatus == PlayerStatus.PLAYING) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + playerLock.unlock(); } - - playerLock.unlock(); } }); - } }; + public void endPlayback() { executor.submit(new Runnable() { @Override public void run() { playerLock.lock(); + releaseWifiLockIfNecessary(); if (playerStatus != PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.INDETERMINATE, media); @@ -774,11 +788,13 @@ public class PlaybackServiceMediaPlayer { @Override public void run() { playerLock.lock(); + releaseWifiLockIfNecessary(); if (playerStatus == PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.STOPPED, null); } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); + if (BuildConfig.DEBUG) + Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); } playerLock.unlock(); @@ -786,6 +802,23 @@ public class PlaybackServiceMediaPlayer { }); } + private synchronized void acquireWifiLockIfNecessary() { + if (stream) { + if (wifiLock == null) { + wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) + .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); + wifiLock.setReferenceCounted(false); + } + wifiLock.acquire(); + } + } + + private synchronized void releaseWifiLockIfNecessary() { + if (wifiLock != null && wifiLock.isHeld()) { + wifiLock.release(); + } + } + /** * Holds information about a PSMP object. */ diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java index e49ea4f83..0924c30ec 100644 --- a/src/de/danoeh/antennapod/storage/DBReader.java +++ b/src/de/danoeh/antennapod/storage/DBReader.java @@ -262,6 +262,9 @@ public final class DBReader { chapter = new VorbisCommentChapter(start, title, item, link); break; + case MP4Chapter.CHAPTERTYPE_MP4CHAPTER: + chapter = new MP4Chapter(start, title, item, link); + break; } if (chapter != null) { chapter.setId(chapterCursor diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java index 8d0ffd9c1..a230ba797 100644 --- a/src/de/danoeh/antennapod/storage/DBTasks.java +++ b/src/de/danoeh/antennapod/storage/DBTasks.java @@ -872,7 +872,7 @@ public final class DBTasks { item.getFlattrStatus().setFlattrQueue(); DBWriter.setFlattredStatus(context, item, true); } else { - FlattrUtils.showNoTokenDialog(context, item.getPaymentLink()); + FlattrUtils.showNoTokenDialogOrRedirect(context, item.getPaymentLink()); } } @@ -888,7 +888,7 @@ public final class DBTasks { feed.getFlattrStatus().setFlattrQueue(); DBWriter.setFlattredStatus(context, feed, true); } else { - FlattrUtils.showNoTokenDialog(context, feed.getPaymentLink()); + FlattrUtils.showNoTokenDialogOrRedirect(context, feed.getPaymentLink()); } } diff --git a/src/de/danoeh/antennapod/util/BitmapDecoder.java b/src/de/danoeh/antennapod/util/BitmapDecoder.java deleted file mode 100644 index 5296d675a..000000000 --- a/src/de/danoeh/antennapod/util/BitmapDecoder.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Rect; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.asynctask.ImageLoader; -import org.apache.commons.io.IOUtils; - -import java.io.InputStream; - -public class BitmapDecoder { - private static final String TAG = "BitmapDecoder"; - - private static int calculateSampleSize(int preferredLength, int length) { - int sampleSize = 1; - if (length > preferredLength) { - sampleSize = Math.round(((float) length / (float) preferredLength)); - } - return sampleSize; - } - - public static Bitmap decodeBitmapFromWorkerTaskResource(int preferredLength, - ImageLoader.ImageWorkerTaskResource source) { - InputStream input = source.openImageInputStream(); - if (input != null) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(input, new Rect(), options); - int srcWidth = options.outWidth; - int srcHeight = options.outHeight; - int length = Math.max(srcWidth, srcHeight); - int sampleSize = calculateSampleSize(preferredLength, length); - if (BuildConfig.DEBUG) - Log.d(TAG, "Using samplesize " + sampleSize); - options.inJustDecodeBounds = false; - options.inSampleSize = sampleSize; - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - Bitmap decodedBitmap = BitmapFactory.decodeStream(source.reopenImageInputStream(input), - null, options); - if (decodedBitmap == null) { - decodedBitmap = BitmapFactory.decodeStream(source.reopenImageInputStream(input)); - } - IOUtils.closeQuietly(input); - return decodedBitmap; - } - return null; - } -} diff --git a/src/de/danoeh/antennapod/util/ChapterUtils.java b/src/de/danoeh/antennapod/util/ChapterUtils.java index 9e1c50674..4a953703a 100644 --- a/src/de/danoeh/antennapod/util/ChapterUtils.java +++ b/src/de/danoeh/antennapod/util/ChapterUtils.java @@ -3,17 +3,22 @@ package de.danoeh.antennapod.util; import android.util.Log; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.MP4Chapter; import de.danoeh.antennapod.util.comparator.ChapterStartTimeComparator; import de.danoeh.antennapod.util.id3reader.ChapterReader; import de.danoeh.antennapod.util.id3reader.ID3ReaderException; import de.danoeh.antennapod.util.playback.Playable; import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentChapterReader; import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException; +import wseemann.media.FFmpegChapter; +import wseemann.media.FFmpegMediaMetadataRetriever; + import org.apache.commons.io.IOUtils; import java.io.*; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -190,6 +195,30 @@ public class ChapterUtils { } } + private static void readMP4ChaptersFromFileUrl(Playable p) { + if (!FFmpegMediaMetadataRetriever.LIB_AVAILABLE) { + if (BuildConfig.DEBUG) Log.d(TAG, "FFmpegMediaMetadataRetriever not available on this architecture"); + return; + } + if (BuildConfig.DEBUG) Log.d(TAG, "Trying to read mp4 chapters from file " + p.getEpisodeTitle()); + + FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever(); + retriever.setDataSource(p.getLocalMediaUrl()); + FFmpegChapter[] res = retriever.getChapters(); + retriever.release(); + if (res != null) { + List<Chapter> chapters = new ArrayList<Chapter>(); + for (FFmpegChapter fFmpegChapter : res) { + chapters.add(new MP4Chapter(fFmpegChapter)); + } + Collections.sort(chapters, new ChapterStartTimeComparator()); + processChapters(chapters, p); + p.setChapters(chapters); + } else { + if (BuildConfig.DEBUG) Log.d(TAG, "No mp4 chapters found in " + p.getEpisodeTitle()); + } + } + /** Makes sure that chapter does a title and an item attribute. */ private static void processChapters(List<Chapter> chapters, Playable p) { for (int i = 0; i < chapters.size(); i++) { @@ -254,6 +283,9 @@ public class ChapterUtils { if (media.getChapters() == null) { ChapterUtils.readOggChaptersFromPlayableFileUrl(media); } + if (media.getChapters() == null) { + ChapterUtils.readMP4ChaptersFromFileUrl(media); + } } else { Log.e(TAG, "Could not load chapters from file url: local file not available"); } diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrConfig.java.example b/src/de/danoeh/antennapod/util/flattr/FlattrConfig.java.example deleted file mode 100644 index da16069ec..000000000 --- a/src/de/danoeh/antennapod/util/flattr/FlattrConfig.java.example +++ /dev/null @@ -1,7 +0,0 @@ -package de.danoeh.antennapod.util.flattr; - -/** Contains credentials to access the Flattr API*/ -public class FlattrConfig { - static final String APP_KEY = ""; - static final String APP_SECRET = ""; -} diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java b/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java index 9809f69a3..96d3bbedd 100644 --- a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java +++ b/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java @@ -9,12 +9,8 @@ import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -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; + +import org.apache.commons.lang3.StringUtils; import org.shredzone.flattr4j.FlattrService; import org.shredzone.flattr4j.exception.FlattrException; import org.shredzone.flattr4j.model.Flattr; @@ -23,252 +19,287 @@ import org.shredzone.flattr4j.oauth.AccessToken; import org.shredzone.flattr4j.oauth.AndroidAuthenticator; import org.shredzone.flattr4j.oauth.Scope; -import java.util.*; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.TimeZone; + +import de.danoeh.antennapod.BuildConfig; +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. */ +/** + * Utility methods for doing something with flattr. + */ public class FlattrUtils { - private static final String TAG = "FlattrUtils"; - - private static final String HOST_NAME = "de.danoeh.antennapod"; - - private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken"; - - // Flattr URL for this app. - public static final String APP_URL = "http://antennapod.com"; - // Human-readable flattr-page. - public static final String APP_LINK = "https://flattr.com/thing/745609/"; - public static final String APP_THING_ID = "745609"; - - private static volatile AccessToken cachedToken; - - private static AndroidAuthenticator createAuthenticator() { - return new AndroidAuthenticator(HOST_NAME, FlattrConfig.APP_KEY, - FlattrConfig.APP_SECRET); - } - - public static void startAuthProcess(Context context) throws FlattrException { - AndroidAuthenticator auth = createAuthenticator(); - auth.setScope(EnumSet.of(Scope.FLATTR)); - Intent intent = auth.createAuthenticateIntent(); - context.startActivity(intent); - } - - private static AccessToken retrieveToken() { - if (cachedToken == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Retrieving access token"); - String token = PreferenceManager.getDefaultSharedPreferences( - PodcastApp.getInstance()) - .getString(PREF_ACCESS_TOKEN, null); - if (token != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Found access token. Caching."); - cachedToken = new AccessToken(token); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No access token found"); - return null; - } - } - return cachedToken; - - } - - public static boolean hasToken() { - return retrieveToken() != null; - } - - public static void storeToken(AccessToken token) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Storing token"); - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(PodcastApp.getInstance()).edit(); - if (token != null) { - editor.putString(PREF_ACCESS_TOKEN, token.getToken()); - } else { - editor.putString(PREF_ACCESS_TOKEN, null); - } - editor.commit(); - cachedToken = token; - } - - public static void deleteToken() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting flattr token"); - storeToken(null); - } - - public static Thing getAppThing(Context context) { - FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); - try { - Thing thing = fs.getThing(Thing.withId(APP_THING_ID)); - return thing; - } catch (FlattrException e) { - e.printStackTrace(); - showErrorDialog(context, e.getMessage()); - return null; - } - } - - public static void clickUrl(Context context, String url) - throws FlattrException { - if (hasToken()) { - FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); - fs.click(url); - } else { - Log.e(TAG, "clickUrl was called with null access token"); - } - } - - public static List<Flattr> retrieveFlattredThings() - throws FlattrException { - ArrayList<Flattr> myFlattrs = new ArrayList<Flattr>(); - - 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; - - for (int page = 0; page < FLATTR_MAXPAGE; page++) { - for (Flattr fl: fs.getMyFlattrs(FLATTR_COUNT, page)) { - if (fl.getCreated().after(firstOfMonthDate)) - myFlattrs.add(fl); - else - break; - } - } - - if (BuildConfig.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(); - new FlattrTokenFetcher(context, auth, uri).executeAsync(); - } - - public static void revokeAccessToken(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Revoking access token"); - deleteToken(); - FlattrServiceCreator.deleteFlattrService(); - showRevokeDialog(context); + private static final String TAG = "FlattrUtils"; + + private static final String HOST_NAME = "de.danoeh.antennapod"; + + private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken"; + + // Flattr URL for this app. + public static final String APP_URL = "http://antennapod.com"; + // Human-readable flattr-page. + public static final String APP_LINK = "https://flattr.com/thing/745609/"; + public static final String APP_THING_ID = "745609"; + + private static volatile AccessToken cachedToken; + + private static AndroidAuthenticator createAuthenticator() { + return new AndroidAuthenticator(HOST_NAME, BuildConfig.FLATTR_APP_KEY, + BuildConfig.FLATTR_APP_SECRET); + } + + public static void startAuthProcess(Context context) throws FlattrException { + AndroidAuthenticator auth = createAuthenticator(); + auth.setScope(EnumSet.of(Scope.FLATTR)); + Intent intent = auth.createAuthenticateIntent(); + context.startActivity(intent); + } + + private static AccessToken retrieveToken() { + if (cachedToken == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Retrieving access token"); + String token = PreferenceManager.getDefaultSharedPreferences( + PodcastApp.getInstance()) + .getString(PREF_ACCESS_TOKEN, null); + if (token != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Found access token. Caching."); + cachedToken = new AccessToken(token); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No access token found"); + return null; + } + } + return cachedToken; + + } + + /** + * Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty + */ + public static boolean hasAPICredentials() { + return StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_KEY) + && StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_SECRET); + } + + public static boolean hasToken() { + return retrieveToken() != null; + } + + public static void storeToken(AccessToken token) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Storing token"); + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(PodcastApp.getInstance()).edit(); + if (token != null) { + editor.putString(PREF_ACCESS_TOKEN, token.getToken()); + } else { + editor.putString(PREF_ACCESS_TOKEN, null); + } + editor.commit(); + cachedToken = token; + } + + public static void deleteToken() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting flattr token"); + storeToken(null); + } + + public static Thing getAppThing(Context context) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + try { + Thing thing = fs.getThing(Thing.withId(APP_THING_ID)); + return thing; + } catch (FlattrException e) { + e.printStackTrace(); + showErrorDialog(context, e.getMessage()); + return null; + } + } + + public static void clickUrl(Context context, String url) + throws FlattrException { + if (hasToken()) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + fs.flattr(url); + } else { + Log.e(TAG, "clickUrl was called with null access token"); + } + } + + public static List<Flattr> retrieveFlattredThings() + throws FlattrException { + ArrayList<Flattr> myFlattrs = new ArrayList<Flattr>(); + + 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; + + for (int page = 0; page < FLATTR_MAXPAGE; page++) { + for (Flattr fl : fs.getMyFlattrs(FLATTR_COUNT, page)) { + if (fl.getCreated().after(firstOfMonthDate)) + myFlattrs.add(fl); + else + break; + } + } + + if (BuildConfig.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(); + new FlattrTokenFetcher(context, auth, uri).executeAsync(); + } + + public static void revokeAccessToken(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Revoking access token"); + deleteToken(); + FlattrServiceCreator.deleteFlattrService(); + showRevokeDialog(context); DBWriter.clearAllFlattrStatus(context); } - // ------------------------------------------------ DIALOGS - - public static void showRevokeDialog(final Context context) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.access_revoked_title); - builder.setMessage(R.string.access_revoked_info); - builder.setNeutralButton(android.R.string.ok, new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); - builder.create().show(); - } - - public static void showNoTokenDialog(final Context context, final String url) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating showNoTokenDialog"); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.no_flattr_token_title); - builder.setMessage(R.string.no_flattr_token_msg); - builder.setPositiveButton(R.string.authenticate_now_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - context.startActivity(new Intent(context, - FlattrAuthActivity.class)); - } - - }); - builder.setNegativeButton(R.string.visit_website_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, - uri)); - } - - }); - builder.create().show(); - } - - public static void showForbiddenDialog(final Context context, - final String url) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.action_forbidden_title); - builder.setMessage(R.string.action_forbidden_msg); - builder.setPositiveButton(R.string.authenticate_now_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - context.startActivity(new Intent(context, - FlattrAuthActivity.class)); - } - - }); - builder.setNegativeButton(R.string.visit_website_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, - uri)); - } - - }); - builder.create().show(); - } - - public static void showErrorDialog(final Context context, final String msg) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.error_label); - builder.setMessage(msg); - builder.setNeutralButton(android.R.string.ok, new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); - builder.create().show(); - } + // ------------------------------------------------ DIALOGS + + public static void showRevokeDialog(final Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.access_revoked_title); + builder.setMessage(R.string.access_revoked_info); + builder.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.create().show(); + } + + /** + * Opens a dialog that ask the user to either connect the app with flattr or to be redirected to + * the thing's website. + * If no API credentials are available, the user will immediately be redirected to the thing's website. + * */ + public static void showNoTokenDialogOrRedirect(final Context context, final String url) { + if (hasAPICredentials()) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.no_flattr_token_title); + builder.setMessage(R.string.no_flattr_token_msg); + builder.setPositiveButton(R.string.authenticate_now_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + context.startActivity(new Intent(context, + FlattrAuthActivity.class)); + } + + } + ); + + builder.setNegativeButton(R.string.visit_website_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, + uri)); + } + + } + ); + builder.create().show(); + } else { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + } + + public static void showForbiddenDialog(final Context context, + final String url) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.action_forbidden_title); + builder.setMessage(R.string.action_forbidden_msg); + builder.setPositiveButton(R.string.authenticate_now_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + context.startActivity(new Intent(context, + FlattrAuthActivity.class)); + } + + } + ); + builder.setNegativeButton(R.string.visit_website_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, + uri)); + } + + } + ); + builder.create().show(); + } + + public static void showErrorDialog(final Context context, final String msg) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.error_label); + builder.setMessage(msg); + builder.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.create().show(); + } }
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java index 390498cea..3f6e6ae0a 100644 --- a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java +++ b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java @@ -3,12 +3,14 @@ package de.danoeh.antennapod.util.playback; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.media.MediaMetadataRetriever; +import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.MediaType; import de.danoeh.antennapod.util.ChapterUtils; +import java.io.File; import java.io.InputStream; import java.util.List; import java.util.concurrent.Callable; @@ -224,22 +226,12 @@ public class ExternalMedia implements Playable { } }; - @Override - public InputStream openImageInputStream() { - return new Playable.DefaultPlayableImageLoader(this) - .openImageInputStream(); - } - - @Override - public String getImageLoaderCacheKey() { - return new Playable.DefaultPlayableImageLoader(this) - .getImageLoaderCacheKey(); - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - return new Playable.DefaultPlayableImageLoader(this) - .reopenImageInputStream(input); - } - + @Override + public Uri getImageUri() { + if (localFileAvailable()) { + return new Uri.Builder().scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()).build(); + } else { + return null; + } + } } diff --git a/src/de/danoeh/antennapod/util/playback/IPlayer.java b/src/de/danoeh/antennapod/util/playback/IPlayer.java index 99f53fb52..2d4551b13 100644 --- a/src/de/danoeh/antennapod/util/playback/IPlayer.java +++ b/src/de/danoeh/antennapod/util/playback/IPlayer.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.util.playback; +import android.content.Context; import android.view.SurfaceHolder; import java.io.IOException; @@ -63,4 +64,6 @@ public interface IPlayer { void stop(); public void setVideoScalingMode(int mode); + + public void setWakeMode(Context context, int mode); } diff --git a/src/de/danoeh/antennapod/util/playback/Playable.java b/src/de/danoeh/antennapod/util/playback/Playable.java index 9ed45abfc..004ae56bb 100644 --- a/src/de/danoeh/antennapod/util/playback/Playable.java +++ b/src/de/danoeh/antennapod/util/playback/Playable.java @@ -2,27 +2,23 @@ package de.danoeh.antennapod.util.playback; import android.content.Context; import android.content.SharedPreferences; -import android.media.MediaMetadataRetriever; import android.os.Parcelable; import android.util.Log; -import de.danoeh.antennapod.asynctask.ImageLoader; + +import java.util.List; + +import de.danoeh.antennapod.asynctask.PicassoImageResource; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.feed.MediaType; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.util.ShownotesProvider; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.Validate; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.List; /** * Interface for objects that can be played by the PlaybackService. */ public interface Playable extends Parcelable, - ImageLoader.ImageWorkerTaskResource, ShownotesProvider { + ShownotesProvider, PicassoImageResource { /** * Save information about the playable in a preference so that it can be @@ -208,69 +204,4 @@ public interface Playable extends Parcelable, } } - - /** - * Uses local file as image resource if it is available. - */ - public static class DefaultPlayableImageLoader implements - ImageLoader.ImageWorkerTaskResource { - private Playable playable; - - public DefaultPlayableImageLoader(Playable playable) { - Validate.notNull(playable); - - this.playable = playable; - } - - @Override - public InputStream openImageInputStream() { - if (playable.localFileAvailable() - && playable.getLocalMediaUrl() != null) { - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); - try { - mmr.setDataSource(playable.getLocalMediaUrl()); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - return null; - } - byte[] imgData = mmr.getEmbeddedPicture(); - if (imgData != null) { - return new PublicByteArrayInputStream(imgData); - - } - } - return null; - } - - @Override - public String getImageLoaderCacheKey() { - return playable.getLocalMediaUrl(); - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - if (input instanceof PublicByteArrayInputStream) { - IOUtils.closeQuietly(input); - byte[] imgData = ((PublicByteArrayInputStream) input) - .getByteArray(); - if (imgData != null) { - ByteArrayInputStream out = new ByteArrayInputStream(imgData); - return out; - } - - } - return null; - } - - private static class PublicByteArrayInputStream extends - ByteArrayInputStream { - public PublicByteArrayInputStream(byte[] buf) { - super(buf); - } - - public byte[] getByteArray() { - return buf; - } - } - } } diff --git a/src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java b/src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java new file mode 100644 index 000000000..807552571 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java @@ -0,0 +1,38 @@ +package instrumentationTest.de.test.antennapod.ui; + +import android.test.ActivityInstrumentationTestCase2; + +import com.robotium.solo.Solo; + +import de.danoeh.antennapod.activity.VideoplayerActivity; + +/** + * Test class for VideoplayerActivity + */ +public class VideoplayerActivityTest extends ActivityInstrumentationTestCase2<VideoplayerActivity> { + + private Solo solo; + + public VideoplayerActivityTest() { + super(VideoplayerActivity.class); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + solo = new Solo(getInstrumentation(), getActivity()); + } + + @Override + public void tearDown() throws Exception { + solo.finishOpenedActivities(); + super.tearDown(); + } + + /** + * Test if activity can be started. + */ + public void testStartActivity() throws Exception { + solo.waitForActivity(VideoplayerActivity.class); + } +} diff --git a/src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java b/src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java index 91e5d966f..08fd0d486 100644 --- a/src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java +++ b/src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java @@ -38,7 +38,7 @@ public class URLCheckerTest extends AndroidTestCase { assertEquals("http://example.com", out); } - public void testItcpProtocol() { + public void testItpcProtocol() { final String in = "itpc://example.com"; final String out = URLChecker.prepareURL(in); assertEquals("http://example.com", out); diff --git a/src/wseemann/media/FFmpegChapter.java b/src/wseemann/media/FFmpegChapter.java new file mode 100644 index 000000000..2a359c386 --- /dev/null +++ b/src/wseemann/media/FFmpegChapter.java @@ -0,0 +1,29 @@ +package wseemann.media; + +/** + * Represents a chapter mark returned by FFmpegMediaMetadataRetriever. + * */ +public class FFmpegChapter +{ + private int id; + private String title; + private long start; + + public FFmpegChapter(int id, String title, long start) { + this.id = id; + this.title = title; + this.start = start; + } + + public int getId() { + return id; + } + + public String getTitle() { + return title; + } + + public long getStart() { + return start; + } +} diff --git a/src/wseemann/media/FFmpegMediaMetadataRetriever.java b/src/wseemann/media/FFmpegMediaMetadataRetriever.java new file mode 100644 index 000000000..89fba915c --- /dev/null +++ b/src/wseemann/media/FFmpegMediaMetadataRetriever.java @@ -0,0 +1,595 @@ +/* + * FFmpegMediaMetadataRetriever: A unified interface for retrieving frame + * and meta data from an input media file. + * + * Copyright 2014 William Seemann + * + * 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. + * + * + * Changes by Daniel Oeh: + * - Rewrite of the 'static' section + * - Addition of 'getChapters' method + * + */ + +package wseemann.media; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Map; + +/** + * FFmpegMediaMetadataRetriever class provides a unified interface for retrieving + * frame and meta data from an input media file. + */ +public class FFmpegMediaMetadataRetriever +{ + private final static String TAG = "FFmpegMediaMetadataRetriever"; + + public static boolean LIB_AVAILABLE = false; + + /** + * User defined bitmap configuration. A bitmap configuration describes how pixels are + * stored. This affects the quality (color depth) as well as the ability to display + * transparent/translucent colors. + */ + public static Bitmap.Config IN_PREFERRED_CONFIG; + + @SuppressLint("SdCardPath") + private static final String LIBRARY_PATH = "/data/data/"; + + private static final String [] JNI_LIBRARIES = { + "avutil", + "swscale", + "avcodec", + "avformat", + "ffmpeg_mediametadataretriever_jni" + }; + + static { + /* + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + + StringBuffer path = null; + File file = null; + boolean foundLibs = false; + + for (int j = 0; j < stackTraceElements.length; j++) { + String libraryPath = stackTraceElements[j].getClassName(); + + String [] packageFragments = libraryPath.trim().split("\\."); + + path = new StringBuffer(LIBRARY_PATH); + + for (int i = 0; i < packageFragments.length; i++) { + if (i > 0) { + path.append("."); + } + + path.append(packageFragments[i]); + try { + //System.load(path.toString() + "/lib/" + JNI_LIBRARIES[0]); + file = new File(path.toString() + "/lib/" + JNI_LIBRARIES[0]); + if (file.exists()) { + path.append("/lib/"); + foundLibs = true; + break; + } + } catch (UnsatisfiedLinkError ex) { + } + } + + if (foundLibs) { + break; + } + } + + // Since libraries for some architectures have been excluded from the source in order to save + // space, this class might not work on all devices. + if (!foundLibs) { + Log.e(TAG, TAG + " libraries not found. Did you forget to add them to your libs folder?"); + //throw new UnsatisfiedLinkError(); + LIB_AVAILABLE = false; + } else { + LIB_AVAILABLE = true; + for (int i = 0; i < JNI_LIBRARIES.length; i++) { + System.load(path.toString() + JNI_LIBRARIES[i]); + } + + native_init(); + }*/ + try { + for (int i = 0; i < JNI_LIBRARIES.length; i++) { + System.loadLibrary(JNI_LIBRARIES[i]); + } + LIB_AVAILABLE = true; + native_init(); + } catch (UnsatisfiedLinkError e) { + Log.e(TAG, "Library not found"); + LIB_AVAILABLE = false; + } + } + + // The field below is accessed by native methods + private int mNativeContext; + + public FFmpegMediaMetadataRetriever() { + native_setup(); + } + + /** + * Sets the data source (file pathname) to use. Call this + * method before the rest of the methods in this class. This method may be + * time-consuming. + * + * @param path The path of the input media file. + * @throws IllegalArgumentException If the path is invalid. + */ + public native void setDataSource(String path) throws IllegalArgumentException; + + /** + * Sets the data source (URI) to use. Call this + * method before the rest of the methods in this class. This method may be + * time-consuming. + * + * @param uri The URI of the input media. + * @param headers the headers to be sent together with the request for the data + * @throws IllegalArgumentException If the URI is invalid. + */ + public void setDataSource(String uri, Map<String, String> headers) + throws IllegalArgumentException { + int i = 0; + String[] keys = new String[headers.size()]; + String[] values = new String[headers.size()]; + for (Map.Entry<String, String> entry: headers.entrySet()) { + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + ++i; + } + _setDataSource(uri, keys, values); + } + + private native void _setDataSource( + String uri, String[] keys, String[] values) + throws IllegalArgumentException; + + /** + * Sets the data source (FileDescriptor) to use. It is the caller's + * responsibility to close the file descriptor. It is safe to do so as soon + * as this call returns. Call this method before the rest of the methods in + * this class. This method may be time-consuming. + * + * @param fd the FileDescriptor for the file you want to play + * @param offset the offset into the file where the data to be played starts, + * in bytes. It must be non-negative + * @param length the length in bytes of the data to be played. It must be + * non-negative. + * @throws IllegalArgumentException if the arguments are invalid + */ + public native void setDataSource(FileDescriptor fd, long offset, long length) + throws IllegalArgumentException; + + /** + * Sets the data source (FileDescriptor) to use. It is the caller's + * responsibility to close the file descriptor. It is safe to do so as soon + * as this call returns. Call this method before the rest of the methods in + * this class. This method may be time-consuming. + * + * @param fd the FileDescriptor for the file you want to play + * @throws IllegalArgumentException if the FileDescriptor is invalid + */ + public void setDataSource(FileDescriptor fd) + throws IllegalArgumentException { + // intentionally less than LONG_MAX + setDataSource(fd, 0, 0x7ffffffffffffffL); + } + + /** + * Sets the data source as a content Uri. Call this method before + * the rest of the methods in this class. This method may be time-consuming. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * @throws IllegalArgumentException if the Uri is invalid + * @throws SecurityException if the Uri cannot be used due to lack of + * permission. + */ + public void setDataSource(Context context, Uri uri) + throws IllegalArgumentException, SecurityException { + if (uri == null) { + throw new IllegalArgumentException(); + } + + String scheme = uri.getScheme(); + if(scheme == null || scheme.equals("file")) { + setDataSource(uri.getPath()); + return; + } + + AssetFileDescriptor fd = null; + try { + ContentResolver resolver = context.getContentResolver(); + try { + fd = resolver.openAssetFileDescriptor(uri, "r"); + } catch(FileNotFoundException e) { + throw new IllegalArgumentException(); + } + if (fd == null) { + throw new IllegalArgumentException(); + } + FileDescriptor descriptor = fd.getFileDescriptor(); + if (!descriptor.valid()) { + throw new IllegalArgumentException(); + } + // Note: using getDeclaredLength so that our behavior is the same + // as previous versions when the content provider is returning + // a full file. + if (fd.getDeclaredLength() < 0) { + setDataSource(descriptor); + } else { + setDataSource(descriptor, fd.getStartOffset(), fd.getDeclaredLength()); + } + return; + } catch (SecurityException ex) { + } finally { + try { + if (fd != null) { + fd.close(); + } + } catch(IOException ioEx) { + } + } + setDataSource(uri.toString()); + } + + /** + * Call this method after setDataSource(). This method retrieves the + * meta data value associated with the keyCode. + * + * The keyCode currently supported is listed below as METADATA_XXX + * constants. With any other value, it returns a null pointer. + * + * @param keyCode One of the constants listed below at the end of the class. + * @return The meta data value associate with the given keyCode on success; + * null on failure. + */ + public native String extractMetadata(String key); + + /** + * Call this method after setDataSource(). This method finds a + * representative frame close to the given time position by considering + * the given option if possible, and returns it as a bitmap. This is + * useful for generating a thumbnail for an input data source or just + * obtain and display a frame at the given time position. + * + * @param timeUs The time position where the frame will be retrieved. + * When retrieving the frame at the given time position, there is no + * guarantee that the data source has a frame located at the position. + * When this happens, a frame nearby will be returned. If timeUs is + * negative, time position and option will ignored, and any frame + * that the implementation considers as representative may be returned. + * + * @param option a hint on how the frame is found. Use + * {@link #OPTION_PREVIOUS_SYNC} if one wants to retrieve a sync frame + * that has a timestamp earlier than or the same as timeUs. Use + * {@link #OPTION_NEXT_SYNC} if one wants to retrieve a sync frame + * that has a timestamp later than or the same as timeUs. Use + * {@link #OPTION_CLOSEST_SYNC} if one wants to retrieve a sync frame + * that has a timestamp closest to or the same as timeUs. Use + * {@link #OPTION_CLOSEST} if one wants to retrieve a frame that may + * or may not be a sync frame but is closest to or the same as timeUs. + * {@link #OPTION_CLOSEST} often has larger performance overhead compared + * to the other options if there is no sync frame located at timeUs. + * + * @return A Bitmap containing a representative video frame, which + * can be null, if such a frame cannot be retrieved. + */ + public Bitmap getFrameAtTime(long timeUs, int option) { + if (option < OPTION_PREVIOUS_SYNC || + option > OPTION_CLOSEST) { + throw new IllegalArgumentException("Unsupported option: " + option); + } + + Bitmap b = null; + + BitmapFactory.Options bitmapOptionsCache = new BitmapFactory.Options(); + bitmapOptionsCache.inPreferredConfig = getInPreferredConfig(); + bitmapOptionsCache.inDither = false; + + byte [] picture = _getFrameAtTime(timeUs, option); + + if (picture != null) { + b = BitmapFactory.decodeByteArray(picture, 0, picture.length, bitmapOptionsCache); + } + + return b; + } + + /** + * Call this method after setDataSource(). This method finds a + * representative frame close to the given time position if possible, + * and returns it as a bitmap. This is useful for generating a thumbnail + * for an input data source. Call this method if one does not care + * how the frame is found as long as it is close to the given time; + * otherwise, please call {@link #getFrameAtTime(long, int)}. + * + * @param timeUs The time position where the frame will be retrieved. + * When retrieving the frame at the given time position, there is no + * guarentee that the data source has a frame located at the position. + * When this happens, a frame nearby will be returned. If timeUs is + * negative, time position and option will ignored, and any frame + * that the implementation considers as representative may be returned. + * + * @return A Bitmap containing a representative video frame, which + * can be null, if such a frame cannot be retrieved. + * + * @see #getFrameAtTime(long, int) + */ + public Bitmap getFrameAtTime(long timeUs) { + Bitmap b = null; + + BitmapFactory.Options bitmapOptionsCache = new BitmapFactory.Options(); + bitmapOptionsCache.inPreferredConfig = getInPreferredConfig(); + bitmapOptionsCache.inDither = false; + + byte [] picture = _getFrameAtTime(timeUs, OPTION_CLOSEST_SYNC); + + if (picture != null) { + b = BitmapFactory.decodeByteArray(picture, 0, picture.length, bitmapOptionsCache); + } + + return b; + } + + /** + * Call this method after setDataSource(). This method finds a + * representative frame at any time position if possible, + * and returns it as a bitmap. This is useful for generating a thumbnail + * for an input data source. Call this method if one does not + * care about where the frame is located; otherwise, please call + * {@link #getFrameAtTime(long)} or {@link #getFrameAtTime(long, int)} + * + * @return A Bitmap containing a representative video frame, which + * can be null, if such a frame cannot be retrieved. + * + * @see #getFrameAtTime(long) + * @see #getFrameAtTime(long, int) + */ + public Bitmap getFrameAtTime() { + return getFrameAtTime(-1, OPTION_CLOSEST_SYNC); + } + + /** + * Call this method after setDataSource(). This method finds any + * chapter marks that are contained in the media file. + * + * @return An array of FFmpegChapter objects or null if no chapters + * could be found. + * */ + public native FFmpegChapter[] getChapters(); + + private native byte [] _getFrameAtTime(long timeUs, int option); + + /** + * Call this method after setDataSource(). This method finds the optional + * graphic or album/cover art associated associated with the data source. If + * there are more than one pictures, (any) one of them is returned. + * + * @return null if no such graphic is found. + */ + public native byte[] getEmbeddedPicture(); + + /** + * Call it when one is done with the object. This method releases the memory + * allocated internally. + */ + public native void release(); + private native void native_setup(); + private static native void native_init(); + + private native final void native_finalize(); + + @Override + protected void finalize() throws Throwable { + try { + native_finalize(); + } finally { + super.finalize(); + } + } + + private Bitmap.Config getInPreferredConfig() { + if (IN_PREFERRED_CONFIG != null) { + return IN_PREFERRED_CONFIG; + } + + return Bitmap.Config.RGB_565; + } + + /** + * Option used in method {@link #getFrameAtTime(long, int)} to get a + * frame at a specified location. + * + * @see #getFrameAtTime(long, int) + */ + /* Do not change these option values without updating their counterparts + * in jni/metadata/ffmpeg_mediametadataretriever.h! + */ + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a sync (or key) frame associated with a data source that is located + * right before or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + public static final int OPTION_PREVIOUS_SYNC = 0x00; + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a sync (or key) frame associated with a data source that is located + * right after or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + public static final int OPTION_NEXT_SYNC = 0x01; + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a sync (or key) frame associated with a data source that is located + * closest to (in time) or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + public static final int OPTION_CLOSEST_SYNC = 0x02; + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a frame (not necessarily a key frame) associated with a data source that + * is located closest to or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + public static final int OPTION_CLOSEST = 0x03; + + /** + * The metadata key to retrieve the name of the set this work belongs to. + */ + public static final String METADATA_KEY_ALBUM = "album"; + /** + * The metadata key to retrieve the main creator of the set/album, if different + * from artist. e.g. "Various Artists" for compilation albums. + */ + public static final String METADATA_KEY_ALBUM_ARTIST = "album_artist"; + /** + * The metadata key to retrieve the main creator of the work. + */ + public static final String METADATA_KEY_ARTIST = "artist"; + /** + * The metadata key to retrieve the any additional description of the file. + */ + public static final String METADATA_KEY_COMMENT = "comment"; + /** + * The metadata key to retrieve the who composed the work, if different from artist. + */ + public static final String METADATA_KEY_COMPOSER = "composer"; + /** + * The metadata key to retrieve the name of copyright holder. + */ + public static final String METADATA_KEY_COPYRIGHT = "copyright"; + /** + * The metadata key to retrieve the date when the file was created, preferably in ISO 8601. + */ + public static final String METADATA_KEY_CREATION_TIME = "creation_time"; + /** + * The metadata key to retrieve the date when the work was created, preferably in ISO 8601. + */ + public static final String METADATA_KEY_DATE = "date"; + /** + * The metadata key to retrieve the number of a subset, e.g. disc in a multi-disc collection. + */ + public static final String METADATA_KEY_DISC = "disc"; + /** + * The metadata key to retrieve the name/settings of the software/hardware that produced the file. + */ + public static final String METADATA_KEY_ENCODER = "encoder"; + /** + * The metadata key to retrieve the person/group who created the file. + */ + public static final String METADATA_KEY_ENCODED_BY = "encoded_by"; + /** + * The metadata key to retrieve the original name of the file. + */ + public static final String METADATA_KEY_FILENAME = "filename"; + /** + * The metadata key to retrieve the genre of the work. + */ + public static final String METADATA_KEY_GENRE = "genre"; + /** + * The metadata key to retrieve the main language in which the work is performed, preferably + * in ISO 639-2 format. Multiple languages can be specified by separating them with commas. + */ + public static final String METADATA_KEY_LANGUAGE = "language"; + /** + * The metadata key to retrieve the artist who performed the work, if different from artist. + * E.g for "Also sprach Zarathustra", artist would be "Richard Strauss" and performer "London + * Philharmonic Orchestra". + */ + public static final String METADATA_KEY_PERFORMER = "performer"; + /** + * The metadata key to retrieve the name of the label/publisher. + */ + public static final String METADATA_KEY_PUBLISHER = "publisher"; + /** + * The metadata key to retrieve the name of the service in broadcasting (channel name). + */ + public static final String METADATA_KEY_SERVICE_NAME = "service_name"; + /** + * The metadata key to retrieve the name of the service provider in broadcasting. + */ + public static final String METADATA_KEY_SERVICE_PROVIDER = "service_provider"; + /** + * The metadata key to retrieve the name of the work. + */ + public static final String METADATA_KEY_TITLE = "title"; + /** + * The metadata key to retrieve the number of this work in the set, can be in form current/total. + */ + public static final String METADATA_KEY_TRACK = "track"; + /** + * The metadata key to retrieve the total bitrate of the bitrate variant that the current stream + * is part of. + */ + public static final String METADATA_KEY_VARIANT_BITRATE = "bitrate"; + /** + * The metadata key to retrieve the duration of the work in milliseconds. + */ + public static final String METADATA_KEY_DURATION = "duration"; + /** + * The metadata key to retrieve the audio codec of the work. + */ + public static final String METADATA_KEY_AUDIO_CODEC = "audio_codec"; + /** + * The metadata key to retrieve the video codec of the work. + */ + public static final String METADATA_KEY_VIDEO_CODEC = "video_codec"; + /** + * This key retrieves the video rotation angle in degrees, if available. + * The video rotation angle may be 0, 90, 180, or 270 degrees. + */ + public static final String METADATA_KEY_VIDEO_ROTATION = "rotate"; + /** + * The metadata key to retrieve the main creator of the work. + */ + public static final String METADATA_KEY_ICY_METADATA = "icy_metadata"; + /** + * The metadata key to retrieve the main creator of the work. + */ + //private static final String METADATA_KEY_ICY_ARTIST = "icy_artist"; + /** + * The metadata key to retrieve the name of the work. + */ + //private static final String METADATA_KEY_ICY_TITLE = "icy_title"; + /** + * This metadata key retrieves the average framerate (in frames/sec), if available. + */ + public static final String METADATA_KEY_FRAMERATE = "framerate"; +} |