summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml4
-rw-r--r--CHANGELOG.md12
-rw-r--r--README.md11
-rw-r--r--assets/LICENSE_FFMPEGMEDIAMETADATARETRIEVER.txt16
-rw-r--r--assets/LICENSE_OKHTTP.txt11
-rw-r--r--assets/LICENSE_OKIO.txt202
-rw-r--r--assets/LICENSE_PICASSO.txt13
-rw-r--r--assets/about.html26
-rw-r--r--build.gradle25
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin51106 -> 51017 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties4
-rwxr-xr-xjniLibs/armeabi/libavcodec.sobin0 -> 4437288 bytes
-rwxr-xr-xjniLibs/armeabi/libavformat.sobin0 -> 795008 bytes
-rwxr-xr-xjniLibs/armeabi/libavutil.sobin0 -> 165348 bytes
-rw-r--r--jniLibs/armeabi/libcrypto.sobin0 -> 853804 bytes
-rwxr-xr-xjniLibs/armeabi/libffmpeg_mediametadataretriever_jni.sobin0 -> 34236 bytes
-rw-r--r--jniLibs/armeabi/libssl.sobin0 -> 197292 bytes
-rwxr-xr-xjniLibs/armeabi/libswscale.sobin0 -> 251228 bytes
-rw-r--r--pom.xml2
-rw-r--r--proguard.cfg4
-rw-r--r--res/layout/feeditem_dialog.xml3
-rw-r--r--res/values-ca/strings.xml11
-rw-r--r--res/values-fr/strings.xml10
-rw-r--r--res/values-pt/strings.xml12
-rw-r--r--res/values-sv-rSE/strings.xml15
-rw-r--r--src/com/aocate/media/ServiceBackedMediaPlayer.java33
-rw-r--r--src/de/danoeh/antennapod/AppConfig.java2
-rw-r--r--src/de/danoeh/antennapod/PodcastApp.java8
-rw-r--r--src/de/danoeh/antennapod/activity/AudioplayerActivity.java16
-rw-r--r--src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java37
-rw-r--r--src/de/danoeh/antennapod/activity/FeedInfoActivity.java8
-rw-r--r--src/de/danoeh/antennapod/activity/MediaplayerActivity.java14
-rw-r--r--src/de/danoeh/antennapod/activity/PreferenceActivity.java2
-rw-r--r--src/de/danoeh/antennapod/activity/VideoplayerActivity.java5
-rw-r--r--src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java6
-rw-r--r--src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java17
-rw-r--r--src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java527
-rw-r--r--src/de/danoeh/antennapod/adapter/NavListAdapter.java8
-rw-r--r--src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java19
-rw-r--r--src/de/danoeh/antennapod/adapter/QueueListAdapter.java14
-rw-r--r--src/de/danoeh/antennapod/adapter/SearchlistAdapter.java121
-rw-r--r--src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java17
-rw-r--r--src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java115
-rw-r--r--src/de/danoeh/antennapod/asynctask/CachedBitmap.java27
-rw-r--r--src/de/danoeh/antennapod/asynctask/ImageDiskCache.java397
-rw-r--r--src/de/danoeh/antennapod/asynctask/ImageLoader.java246
-rw-r--r--src/de/danoeh/antennapod/asynctask/PicassoImageResource.java37
-rw-r--r--src/de/danoeh/antennapod/asynctask/PicassoProvider.java152
-rw-r--r--src/de/danoeh/antennapod/feed/Feed.java14
-rw-r--r--src/de/danoeh/antennapod/feed/FeedComponent.java82
-rw-r--r--src/de/danoeh/antennapod/feed/FeedImage.java42
-rw-r--r--src/de/danoeh/antennapod/feed/FeedItem.java57
-rw-r--r--src/de/danoeh/antennapod/feed/FeedMedia.java70
-rw-r--r--src/de/danoeh/antennapod/feed/MP4Chapter.java27
-rw-r--r--src/de/danoeh/antennapod/fragment/CoverFragment.java146
-rw-r--r--src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java407
-rw-r--r--src/de/danoeh/antennapod/fragment/ItemlistFragment.java10
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java8
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackService.java41
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java133
-rw-r--r--src/de/danoeh/antennapod/storage/DBReader.java3
-rw-r--r--src/de/danoeh/antennapod/storage/DBTasks.java4
-rw-r--r--src/de/danoeh/antennapod/util/BitmapDecoder.java50
-rw-r--r--src/de/danoeh/antennapod/util/ChapterUtils.java32
-rw-r--r--src/de/danoeh/antennapod/util/flattr/FlattrConfig.java.example7
-rw-r--r--src/de/danoeh/antennapod/util/flattr/FlattrUtils.java525
-rw-r--r--src/de/danoeh/antennapod/util/playback/ExternalMedia.java28
-rw-r--r--src/de/danoeh/antennapod/util/playback/IPlayer.java3
-rw-r--r--src/de/danoeh/antennapod/util/playback/Playable.java79
-rw-r--r--src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java38
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java2
-rw-r--r--src/wseemann/media/FFmpegChapter.java29
-rw-r--r--src/wseemann/media/FFmpegMediaMetadataRetriever.java595
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
diff --git a/README.md b/README.md
index b4a74b4f4..503f46717 100644
--- a/README.md
+++ b/README.md
@@ -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
index 3c7abdf12..3d0dee6e8 100644
--- a/gradle/wrapper/gradle-wrapper.jar
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
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
new file mode 100755
index 000000000..c6cd15096
--- /dev/null
+++ b/jniLibs/armeabi/libavcodec.so
Binary files differ
diff --git a/jniLibs/armeabi/libavformat.so b/jniLibs/armeabi/libavformat.so
new file mode 100755
index 000000000..b491bd5b2
--- /dev/null
+++ b/jniLibs/armeabi/libavformat.so
Binary files differ
diff --git a/jniLibs/armeabi/libavutil.so b/jniLibs/armeabi/libavutil.so
new file mode 100755
index 000000000..8451b8ba8
--- /dev/null
+++ b/jniLibs/armeabi/libavutil.so
Binary files differ
diff --git a/jniLibs/armeabi/libcrypto.so b/jniLibs/armeabi/libcrypto.so
new file mode 100644
index 000000000..b7de733d2
--- /dev/null
+++ b/jniLibs/armeabi/libcrypto.so
Binary files differ
diff --git a/jniLibs/armeabi/libffmpeg_mediametadataretriever_jni.so b/jniLibs/armeabi/libffmpeg_mediametadataretriever_jni.so
new file mode 100755
index 000000000..747191cc6
--- /dev/null
+++ b/jniLibs/armeabi/libffmpeg_mediametadataretriever_jni.so
Binary files differ
diff --git a/jniLibs/armeabi/libssl.so b/jniLibs/armeabi/libssl.so
new file mode 100644
index 000000000..623811e3a
--- /dev/null
+++ b/jniLibs/armeabi/libssl.so
Binary files differ
diff --git a/jniLibs/armeabi/libswscale.so b/jniLibs/armeabi/libswscale.so
new file mode 100755
index 000000000..d0a3a18c1
--- /dev/null
+++ b/jniLibs/armeabi/libswscale.so
Binary files differ
diff --git a/pom.xml b/pom.xml
index 658989cee..c90a33f1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -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";
+}