diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2014-02-27 21:10:38 +0100 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2014-02-27 21:10:38 +0100 |
commit | ffb0fdcb1259a677651accec04f302faab0a76d8 (patch) | |
tree | 7bfae6f067817106bb0d4027b3e515d26f02093c | |
parent | b1edf761cc34c837679cea7d68c621dbe49540be (diff) | |
parent | 8ef1a2c37336f36574774ea202a7b410be45be20 (diff) | |
download | AntennaPod-ffb0fdcb1259a677651accec04f302faab0a76d8.zip |
Merge branch 'develop'0.9.8.1
116 files changed, 7808 insertions, 3735 deletions
diff --git a/.tx/config b/.tx/config index b1a6121d6..b5e2efc75 100644 --- a/.tx/config +++ b/.tx/config @@ -15,6 +15,7 @@ trans.fr = res/values-fr/strings.xml trans.hi_IN = res/values-hi-rIN/strings.xml trans.it_IT = res/values-it-rIT/strings.xml trans.nl = res/values-nl/strings.xml +trans.pl_PL = res/values-pl-rPL/strings.xml trans.pt = res/values-pt/strings.xml trans.pt_BR = res/values-pt-rBR/strings.xml trans.ro_RO = res/values-ro-rRO/strings.xml diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cf3f1e7b3..979edf4ce 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,15 +1,16 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.danoeh.antennapod" - android:versionCode="33" - android:versionName="0.9.8.0" > + android:versionCode="34" + android:versionName="0.9.8.1" > <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-sdk android:minSdkVersion="10" - android:targetSdkVersion="18" /> + android:targetSdkVersion="19" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> @@ -153,7 +154,7 @@ android:name=".service.download.DownloadService" android:enabled="true"/> <service - android:name="de.danoeh.antennapod.service.PlaybackService" + android:name=".service.playback.PlaybackService" android:enabled="true" android:exported="true"> </service> @@ -194,7 +195,7 @@ </activity> <service - android:name=".service.PlayerWidgetService" + android:name=".service.playback.PlayerWidgetService" android:enabled="true" android:exported="false"> </service> @@ -327,7 +328,7 @@ android:configChanges="keyboardHidden|orientation"> <meta-data android:name="android.support.PARENT_ACTIVITY" - android:value="de.danoeh.antennapod.activity.MiroGuideMainActiviy" /> + android:value="de.danoeh.antennapod.activity.MiroGuideMainActivity" /> </activity> <activity android:name=".activity.MiroGuideChannelViewActivity" diff --git a/CHANGELOG.md b/CHANGELOG.md index a952a3e6b..4b15d61ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Change Log ========== +Version 0.9.8.1 +--------------- +* Added option to flattr an episode automatically after 80 percent of the episode have been played +* Added Polish translation +* Several bugfixes and improvements + Version 0.9.8.0 --------------- * Added access to the gpodder.net directory diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 1a358ac8c..aec361b63 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -8,6 +8,7 @@ LatinSuD wseemann hzulla andrewgaul +peschmae0 Translations: @@ -28,3 +29,4 @@ Portuguese: smarquespt Swedish: nilso, Bio, TwoD, bpnilsson Hindi: siddhusengar Dutch: e2jk +Polish (Poland): Mephistofeles, shark103, tyle diff --git a/assets/about.html b/assets/about.html index 3aec42ea1..c62f2b1d5 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.8.0</p> + <p>AntennaPod, Version 0.9.8.1</p> <p>Copyright © 2012 Daniel Oeh</p> diff --git a/build.gradle b/build.gradle index 20166fa52..33d597340 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.6.+' + classpath 'com.android.tools.build:gradle:0.8.+' } } apply plugin: 'android' @@ -23,9 +23,9 @@ dependencies { new URL('http://www.aocate.com/presto/client/presto_client-0.8.5.jar').withInputStream{ i -> prestoLib.withOutputStream{ it << i }} } - compile 'com.android.support:appcompat-v7:18.0.+' - compile 'org.apache.commons:commons-lang3:3.1' - compile ('org.shredzone.flattr4j:flattr4j-core:2.7') { + compile 'com.android.support:appcompat-v7:19.0.+' + compile 'org.apache.commons:commons-lang3:3.2.1' + compile ('org.shredzone.flattr4j:flattr4j-core:2.8') { exclude group: 'org.apache.httpcomponents', module: 'httpcore' exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.json', module: 'json' @@ -37,12 +37,12 @@ dependencies { } android { - compileSdkVersion 18 - buildToolsVersion "18.1.0" + compileSdkVersion 19 + buildToolsVersion "19.0.1" defaultConfig { minSdkVersion 10 - targetSdkVersion 18 + targetSdkVersion 19 testPackageName "de.test.antennapod" testInstrumentationRunner "instrumentationTest.de.test.antennapod.AntennaPodTestRunner" } @@ -94,4 +94,9 @@ android { signingConfig signingConfigs.releaseConfig } } + + packagingOptions { + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE.txt' + } } @@ -5,7 +5,7 @@ <groupId>de.danoeh</groupId> <artifactId>antennapod</artifactId> <packaging>apk</packaging> - <version>0.9.8.0</version> + <version>0.9.8.1</version> <name>AntennaPod</name> @@ -13,18 +13,18 @@ <dependency> <groupId>android.support</groupId> <artifactId>compatibility-v4</artifactId> - <version>18</version> + <version>19</version> </dependency> <dependency> <groupId>android.support</groupId> <artifactId>compatibility-v7-appcompat</artifactId> - <version>18</version> + <version>19</version> <type>apklib</type> </dependency> <dependency> <groupId>android.support</groupId> <artifactId>compatibility-v7-appcompat</artifactId> - <version>18</version> + <version>19</version> <type>jar</type> </dependency> <dependency> @@ -58,15 +58,16 @@ </exclusions> </dependency> <dependency> - <groupId>com.google.android</groupId> + <groupId>android</groupId> <artifactId>android</artifactId> <scope>provided</scope> - <version>4.1.1.4</version> + <version>4.4_r1</version> </dependency> <dependency> - <groupId>com.google.android</groupId> + <groupId>com.google.android.annotations</groupId> <artifactId>annotations</artifactId> - <version>4.1.1.4</version> + <version>22.3</version> + <scope>provided</scope> </dependency> <dependency> <groupId>commons-io</groupId> @@ -109,11 +110,11 @@ <plugin> <groupId>com.jayway.maven.plugins.android.generation2</groupId> <artifactId>android-maven-plugin</artifactId> - <version>3.6.1</version> + <version>3.8.0</version> <configuration> <sdk> <path>${env.ANDROID_HOME}</path> - <platform>18</platform> + <platform>19</platform> </sdk> <manifest> <debuggable>true</debuggable> diff --git a/project.properties b/project.properties index 75f295e31..57e7d75fe 100644 --- a/project.properties +++ b/project.properties @@ -9,7 +9,6 @@ # Project target. proguard.config=proguard.cfg -target=android-18 -android.library.reference.1=submodules/ActionBarSherlock/library -android.library.reference.2=submodules/ViewPagerIndicator/library -android.library.reference.3=submodules/dslv/library +target=android-19 +android.library.reference.1=submodules/dslv/library + diff --git a/res/drawable-hdpi/ic_action_pause_over_video.png b/res/drawable-hdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..64b07728f --- /dev/null +++ b/res/drawable-hdpi/ic_action_pause_over_video.png diff --git a/res/drawable-hdpi/ic_action_play_over_video.png b/res/drawable-hdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..a364ca7c2 --- /dev/null +++ b/res/drawable-hdpi/ic_action_play_over_video.png diff --git a/res/drawable-mdpi/ic_action_pause_over_video.png b/res/drawable-mdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..f478ac321 --- /dev/null +++ b/res/drawable-mdpi/ic_action_pause_over_video.png diff --git a/res/drawable-mdpi/ic_action_play_over_video.png b/res/drawable-mdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..835ff3636 --- /dev/null +++ b/res/drawable-mdpi/ic_action_play_over_video.png diff --git a/res/drawable-xhdpi/ic_action_pause_over_video.png b/res/drawable-xhdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..b0777a023 --- /dev/null +++ b/res/drawable-xhdpi/ic_action_pause_over_video.png diff --git a/res/drawable-xhdpi/ic_action_play_over_video.png b/res/drawable-xhdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..24331a48c --- /dev/null +++ b/res/drawable-xhdpi/ic_action_play_over_video.png diff --git a/res/drawable-xxhdpi/ic_action_pause_over_video.png b/res/drawable-xxhdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..fa85601cf --- /dev/null +++ b/res/drawable-xxhdpi/ic_action_pause_over_video.png diff --git a/res/drawable-xxhdpi/ic_action_play_over_video.png b/res/drawable-xxhdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..121be211e --- /dev/null +++ b/res/drawable-xxhdpi/ic_action_play_over_video.png diff --git a/res/drawable/overlay_button_circle_background.xml b/res/drawable/overlay_button_circle_background.xml new file mode 100644 index 000000000..90c51472c --- /dev/null +++ b/res/drawable/overlay_button_circle_background.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <gradient + android:type="radial" + android:gradientRadius="60" + android:startColor="#000000" + android:endColor="@android:color/transparent"/> +</shape>
\ No newline at end of file diff --git a/res/layout-land/audioplayer_activity.xml b/res/layout-land/audioplayer_activity.xml index 1e671c745..7900e1ced 100644 --- a/res/layout-land/audioplayer_activity.xml +++ b/res/layout-land/audioplayer_activity.xml @@ -26,6 +26,7 @@ <ImageButton android:id="@+id/butNavLeft" + android:contentDescription="@string/show_shownotes_label" android:layout_width="60dp" android:layout_height="match_parent" android:layout_alignParentLeft="true" @@ -34,6 +35,7 @@ <ImageButton android:id="@+id/butNavRight" + android:contentDescription="@string/show_chapters_label" android:layout_width="60dp" android:layout_height="match_parent" android:layout_alignParentRight="true" @@ -89,6 +91,7 @@ <ImageButton android:id="@+id/butPlay" + android:contentDescription="@string/pause_label" android:layout_width="80dp" android:layout_height="match_parent" android:layout_centerHorizontal="true" @@ -97,6 +100,7 @@ <ImageButton android:id="@+id/butRev" + android:contentDescription="@string/rewind_label" android:layout_width="60dp" android:layout_height="match_parent" android:layout_toLeftOf="@id/butPlay" @@ -105,6 +109,7 @@ <ImageButton android:id="@+id/butFF" + android:contentDescription="@string/fast_forward_label" android:layout_width="60dp" android:layout_height="match_parent" android:layout_toRightOf="@id/butPlay" diff --git a/res/layout-land/videoplayer_activity.xml b/res/layout-land/videoplayer_activity.xml index 675d9709d..f1e54f7c3 100644 --- a/res/layout-land/videoplayer_activity.xml +++ b/res/layout-land/videoplayer_activity.xml @@ -1,13 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" > + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - <VideoView + <de.danoeh.antennapod.view.AspectRatioVideoView android:id="@+id/videoview" - android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"/> <ProgressBar android:id="@+id/progressIndicator" @@ -15,58 +16,32 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="invisible" - android:indeterminateOnly="true" /> - <!-- Mediaplayer controls --> + android:indeterminateOnly="true"/> + + <ImageButton + android:id="@+id/butPlay" + android:contentDescription="@string/pause_label" + android:layout_width="80dp" + android:layout_height="80dp" + android:layout_gravity="center" + android:scaleType="fitXY" + android:background="@drawable/overlay_button_circle_background" + android:src="@drawable/ic_action_pause_over_video"/> <LinearLayout android:id="@+id/overlay" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom|center" - android:background="?attr/overlay_background" - android:orientation="vertical" > - - <RelativeLayout - android:id="@+id/playercontrol" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="clip_horizontal" - android:layout_margin="4dp" > - - <ImageButton - android:id="@+id/butPlay" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:background="?attr/borderless_button" - android:src="?attr/av_pause" /> - - <ImageButton - android:id="@+id/butFF" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_marginLeft="8dp" - android:layout_toRightOf="@+id/butPlay" - android:background="?attr/borderless_button" - android:src="?attr/av_fast_forward" /> - - <ImageButton - android:id="@+id/butRev" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_marginRight="8dp" - android:layout_toLeftOf="@+id/butPlay" - android:background="?attr/borderless_button" - android:src="?attr/av_rewind" /> - </RelativeLayout> + android:background="#80000000" + android:orientation="vertical"> <RelativeLayout android:id="@+id/timecontrol" android:layout_width="match_parent" - android:layout_height="30dp" - android:layout_marginBottom="4dp" > + android:layout_height="50dp" + android:paddingTop="8dp" + android:layout_marginBottom="4dp"> <TextView android:id="@+id/txtvPosition" @@ -77,7 +52,10 @@ android:layout_marginBottom="8dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" - android:text="@string/position_default_label" /> + android:layout_marginTop="4dp" + android:textColor="@color/white" + android:textStyle="bold" + android:text="@string/position_default_label"/> <TextView android:id="@+id/txtvLength" @@ -88,7 +66,10 @@ android:layout_marginBottom="8dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" - android:text="@string/position_default_label" /> + android:layout_marginTop="4dp" + android:textColor="@color/white" + android:textStyle="bold" + android:text="@string/position_default_label"/> <SeekBar android:id="@+id/sbPosition" @@ -96,7 +77,7 @@ android:layout_height="wrap_content" android:layout_toLeftOf="@+id/txtvLength" android:layout_toRightOf="@+id/txtvPosition" - android:max="500" /> + android:max="500"/> </RelativeLayout> </LinearLayout> diff --git a/res/layout-v14/directory_chooser.xml b/res/layout-v14/directory_chooser.xml index 3bb1d5c9c..f0bef72e4 100644 --- a/res/layout-v14/directory_chooser.xml +++ b/res/layout-v14/directory_chooser.xml @@ -56,6 +56,7 @@ <ImageButton android:id="@+id/butNavUp" + android:contentDescription="@string/navigate_upwards_label" android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentLeft="true" diff --git a/res/layout/audioplayer_activity.xml b/res/layout/audioplayer_activity.xml index 857d7140f..9b501fbdb 100644 --- a/res/layout/audioplayer_activity.xml +++ b/res/layout/audioplayer_activity.xml @@ -13,6 +13,7 @@ <ImageButton android:id="@+id/butNavLeft" + android:contentDescription="@string/show_shownotes_label" android:layout_width="60dp" android:layout_height="match_parent" android:layout_alignParentLeft="true" @@ -21,6 +22,7 @@ <ImageButton android:id="@+id/butNavRight" + android:contentDescription="@string/show_chapters_label" android:layout_width="60dp" android:layout_height="match_parent" android:layout_alignParentRight="true" @@ -76,6 +78,7 @@ <ImageButton android:id="@+id/butPlay" + android:contentDescription="@string/pause_label" android:layout_width="80dp" android:layout_height="match_parent" android:layout_centerHorizontal="true" @@ -84,6 +87,7 @@ <ImageButton android:id="@+id/butRev" + android:contentDescription="@string/rewind_label" android:layout_width="80dp" android:layout_height="match_parent" android:layout_toLeftOf="@id/butPlay" @@ -92,6 +96,7 @@ <ImageButton android:id="@+id/butFF" + android:contentDescription="@string/fast_forward_label" android:layout_width="80dp" android:layout_height="match_parent" android:layout_toRightOf="@id/butPlay" @@ -100,6 +105,7 @@ <Button android:id="@+id/butPlaybackSpeed" + android:contentDescription="@string/set_playback_speed_label" android:layout_width="80dp" android:layout_height="match_parent" android:layout_toRightOf="@id/butFF" diff --git a/res/layout/cover_fragment.xml b/res/layout/cover_fragment.xml index 9602f1ebc..f9c88ac02 100644 --- a/res/layout/cover_fragment.xml +++ b/res/layout/cover_fragment.xml @@ -7,6 +7,7 @@ <ImageView android:id="@+id/imgvCover" + android:contentDescription="@string/cover_label" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" diff --git a/res/layout/default_feeditemlist_item.xml b/res/layout/default_feeditemlist_item.xml index 28386a264..fdfcca217 100644 --- a/res/layout/default_feeditemlist_item.xml +++ b/res/layout/default_feeditemlist_item.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="0dip" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="0dip" android:layout_height="match_parent" android:layout_weight="1" android:paddingLeft="4dp" > @@ -31,7 +32,8 @@ android:layout_width="@dimen/enc_icons_size" android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvPublished" - android:padding="2dp" /> + android:padding="2dp" + tools:ignore="ContentDescription"/> <TextView android:id="@+id/txtvLenSize" diff --git a/res/layout/directory_chooser.xml b/res/layout/directory_chooser.xml index 842e5d458..738c00842 100644 --- a/res/layout/directory_chooser.xml +++ b/res/layout/directory_chooser.xml @@ -34,6 +34,7 @@ <ImageButton android:id="@+id/butNavUp" + android:contentDescription="@string/navigate_upwards_label" android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentLeft="true" diff --git a/res/layout/external_itemlist_item.xml b/res/layout/external_itemlist_item.xml index 2d0093641..20c63c2cf 100644 --- a/res/layout/external_itemlist_item.xml +++ b/res/layout/external_itemlist_item.xml @@ -1,10 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" android:layout_height="match_parent" > <ImageView android:id="@+id/imgvFeedimage" + android:contentDescription="@string/cover_label" android:layout_width="@dimen/thumbnail_length_itemlist" android:layout_height="@dimen/thumbnail_length_itemlist" android:layout_alignParentLeft="true" @@ -12,6 +14,7 @@ <ImageButton android:id="@+id/butAction" + android:contentDescription="@string/butAction_label" android:layout_width="48dp" android:layout_height="match_parent" android:layout_alignParentBottom="true" @@ -83,7 +86,8 @@ android:layout_height="@dimen/enc_icons_size" android:layout_alignParentRight="true" android:layout_marginLeft="8dp" - android:layout_marginRight="8dp" /> + android:layout_marginRight="8dp" + tools:ignore="ContentDescription"/> <ProgressBar android:id="@+id/pbar_episode_progress" @@ -97,6 +101,7 @@ <ImageView android:id="@+id/statusPlaying" + android:contentDescription="@string/status_playing_label" android:layout_width="@dimen/status_indicator_width" android:layout_height="18dp" android:layout_alignParentRight="true" diff --git a/res/layout/external_player_fragment.xml b/res/layout/external_player_fragment.xml index d777fc8b7..d619d7a77 100644 --- a/res/layout/external_player_fragment.xml +++ b/res/layout/external_player_fragment.xml @@ -24,6 +24,7 @@ <ImageView android:id="@+id/imgvCover" + android:contentDescription="@string/cover_label" android:layout_width="@dimen/external_player_height" android:layout_height="@dimen/external_player_height" android:layout_alignParentLeft="true" @@ -76,6 +77,7 @@ <ImageButton android:id="@+id/butPlay" + android:contentDescription="@string/pause_label" android:layout_width="@dimen/external_player_height" android:layout_height="@dimen/external_player_height" android:background="?attr/borderless_button" /> diff --git a/res/layout/feedinfo.xml b/res/layout/feedinfo.xml index e1c1c10a3..d975ef549 100644 --- a/res/layout/feedinfo.xml +++ b/res/layout/feedinfo.xml @@ -12,6 +12,7 @@ <ImageView android:id="@+id/imgvCover" + android:contentDescription="@string/cover_label" android:layout_width="70dp" android:layout_height="70dp" android:layout_alignParentLeft="true" @@ -97,22 +98,6 @@ </RelativeLayout> <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginLeft="8dp" - android:layout_marginBottom="8dp" - android:layout_marginTop="24dp" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary" - android:text="@string/description_label"/> - - <TextView - android:id="@+id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="8dp"/> - - <TextView android:id="@+id/txtvSettings" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -132,6 +117,23 @@ android:text="@string/auto_download_label" android:enabled="false" android:textColor="?android:attr/textColorPrimary"/> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="8dp" + android:layout_marginBottom="8dp" + android:layout_marginTop="24dp" + android:textSize="@dimen/text_size_medium" + android:textColor="?android:attr/textColorPrimary" + android:text="@string/description_label"/> + + <TextView + android:id="@+id/txtvDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp"/> + </LinearLayout> </ScrollView> diff --git a/res/layout/feeditemlist_header.xml b/res/layout/feeditemlist_header.xml index 31ccb7e96..560013abd 100644 --- a/res/layout/feeditemlist_header.xml +++ b/res/layout/feeditemlist_header.xml @@ -27,6 +27,7 @@ <ImageButton android:id="@+id/butAction" + android:contentDescription="@string/butAction_label" android:layout_width="48dp" android:layout_height="match_parent" android:layout_alignParentBottom="true" diff --git a/res/layout/feeditemlist_item.xml b/res/layout/feeditemlist_item.xml index ed4b0d46d..e2898b601 100644 --- a/res/layout/feeditemlist_item.xml +++ b/res/layout/feeditemlist_item.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="0dip" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="0dip" android:layout_height="match_parent" android:layout_weight="1" android:paddingLeft="4dp" > @@ -45,10 +46,12 @@ android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvPublished" android:layout_toLeftOf="@+id/imgvInPlaylist" - android:padding="2dp" /> + android:padding="2dp" + tools:ignore="ContentDescription"/> <ImageView android:id="@id/imgvInPlaylist" + android:contentDescription="@string/in_queue_label" android:layout_width="@dimen/enc_icons_size" android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvPublished" @@ -59,6 +62,7 @@ <ImageView android:id="@id/imgvDownloaded" + android:contentDescription="@string/status_downloaded_label" android:layout_width="@dimen/enc_icons_size" android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvPublished" @@ -69,6 +73,7 @@ <ImageView android:id="@id/imgvDownloading" + android:contentDescription="@string/downloading_label" android:layout_width="@dimen/enc_icons_size" android:layout_height="@dimen/enc_icons_size" android:layout_below="@id/txtvPublished" @@ -102,6 +107,7 @@ <ImageButton android:id="@id/butAction" + android:contentDescription="@string/butAction_label" android:layout_width="48dp" android:layout_height="match_parent" android:layout_alignParentBottom="true" @@ -118,6 +124,7 @@ <TextView android:id="@+id/statusUnread" + android:contentDescription="@string/status_unread_label" android:layout_width="wrap_content" android:layout_height="18dp" android:layout_alignParentRight="true" @@ -134,6 +141,7 @@ <ImageView android:id="@+id/statusPlaying" + android:contentDescription="@string/status_playing_label" android:layout_width="@dimen/status_indicator_width" android:layout_height="18dp" android:layout_alignParentRight="true" diff --git a/res/layout/feedlist_item.xml b/res/layout/feedlist_item.xml index 90a9e3bf0..f6c43675d 100644 --- a/res/layout/feedlist_item.xml +++ b/res/layout/feedlist_item.xml @@ -1,11 +1,13 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" android:layout_height="match_parent" android:paddingRight="8dp" > <ImageView android:id="@+id/imgvFeedimage" + android:contentDescription="@string/cover_label" android:layout_width="@dimen/thumbnail_length" android:layout_height="@dimen/thumbnail_length" android:layout_alignParentLeft="true" @@ -25,6 +27,7 @@ <RelativeLayout android:id="@+id/lNewStatusLabel" + android:contentDescription="@string/new_episodes_count_label" android:layout_width="@dimen/status_indicator_width" android:layout_height="0dip" android:layout_marginBottom="8dp" @@ -38,7 +41,8 @@ android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="4dp" - android:src="@drawable/white_circle" /> + android:src="@drawable/white_circle" + tools:ignore="ContentDescription"/> <TextView android:id="@+id/txtvNewEps" @@ -57,6 +61,7 @@ <RelativeLayout android:id="@+id/lProgressStatusLabel" + android:contentDescription="@string/in_progress_episodes_count_label" android:layout_width="@dimen/status_indicator_width" android:layout_height="0dip" android:layout_marginBottom="8dp" @@ -84,7 +89,8 @@ android:layout_centerVertical="true" android:layout_marginRight="2dp" android:layout_marginLeft="2dp" - android:src="@drawable/av_play_dark" /> + android:src="@drawable/av_play_dark" + tools:ignore="ContentDescription"/> </RelativeLayout> </LinearLayout> diff --git a/res/layout/feedlist_item_grid.xml b/res/layout/feedlist_item_grid.xml index c761a3a84..934904374 100644 --- a/res/layout/feedlist_item_grid.xml +++ b/res/layout/feedlist_item_grid.xml @@ -1,10 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/imgvFeedimage" + android:contentDescription="@string/cover_label" android:layout_width="@dimen/thumbnail_length" android:layout_height="@dimen/thumbnail_length" android:layout_alignParentTop="true" @@ -25,6 +27,7 @@ <RelativeLayout android:id="@+id/lNewStatusLabel" + android:contentDescription="@string/new_episodes_count_label" android:layout_width="@dimen/status_indicator_width" android:layout_height="0dip" android:layout_marginBottom="8dp" @@ -38,7 +41,8 @@ android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="4dp" - android:src="@drawable/white_circle"/> + android:src="@drawable/white_circle" + tools:ignore="ContentDescription"/> <TextView android:id="@+id/txtvNewEps" @@ -57,6 +61,7 @@ <RelativeLayout android:id="@+id/lProgressStatusLabel" + android:contentDescription="@string/in_progress_episodes_count_label" android:layout_width="@dimen/status_indicator_width" android:layout_height="0dip" android:layout_marginBottom="8dp" @@ -84,7 +89,8 @@ android:layout_centerVertical="true" android:layout_marginLeft="2dp" android:layout_marginRight="2dp" - android:src="@drawable/av_play_dark"/> + android:src="@drawable/av_play_dark" + tools:ignore="ContentDescription"/> </RelativeLayout> </LinearLayout> diff --git a/res/layout/gpodnet_podcast_listitem.xml b/res/layout/gpodnet_podcast_listitem.xml index f6ddb3bd8..1f6cdd1d0 100644 --- a/res/layout/gpodnet_podcast_listitem.xml +++ b/res/layout/gpodnet_podcast_listitem.xml @@ -5,6 +5,7 @@ android:layout_height="match_parent"> <ImageView android:id="@+id/imgvCover" + android:contentDescription="@string/cover_label" android:layout_width="@dimen/thumbnail_length_itemlist" android:layout_height="@dimen/thumbnail_length_itemlist" android:layout_alignParentLeft="true" diff --git a/res/layout/onlinefeedview_header.xml b/res/layout/onlinefeedview_header.xml index 85f0f32f8..441df6174 100644 --- a/res/layout/onlinefeedview_header.xml +++ b/res/layout/onlinefeedview_header.xml @@ -6,6 +6,7 @@ <ImageView android:id="@+id/imgvCover" + android:contentDescription="@string/cover_label" android:layout_width="@dimen/thumbnail_length_onlinefeedview" android:layout_height="@dimen/thumbnail_length_onlinefeedview" android:layout_alignParentLeft="true" diff --git a/res/layout/organize_queue_listitem.xml b/res/layout/organize_queue_listitem.xml index 5cac6e85b..ded2a900b 100644 --- a/res/layout/organize_queue_listitem.xml +++ b/res/layout/organize_queue_listitem.xml @@ -5,6 +5,7 @@ <ImageView android:id="@+id/imgvFeedimage" + android:contentDescription="@string/cover_label" android:layout_width="@dimen/thumbnail_length_itemlist" android:layout_height="@dimen/thumbnail_length_itemlist" android:layout_alignParentLeft="true" diff --git a/res/layout/player_widget.xml b/res/layout/player_widget.xml index 9866da449..b6946f7a8 100644 --- a/res/layout/player_widget.xml +++ b/res/layout/player_widget.xml @@ -11,6 +11,7 @@ <ImageButton android:id="@+id/butPlay" + android:contentDescription="@string/play_label" android:layout_width="56dp" android:layout_height="match_parent" android:layout_alignParentRight="true" diff --git a/res/layout/searchlist_item.xml b/res/layout/searchlist_item.xml index 71c09c16d..889f40eef 100644 --- a/res/layout/searchlist_item.xml +++ b/res/layout/searchlist_item.xml @@ -6,6 +6,7 @@ <ImageView android:id="@+id/imgvFeedimage" + android:contentDescription="@string/cover_label" android:layout_width="55dip" android:layout_height="55dip" android:layout_alignParentLeft="true" diff --git a/res/layout/storage_error.xml b/res/layout/storage_error.xml index f2e412f46..c1ee77262 100644 --- a/res/layout/storage_error.xml +++ b/res/layout/storage_error.xml @@ -5,6 +5,7 @@ <ImageView android:id="@+id/imageView1" + android:contentDescription="@string/external_storage_error_msg" android:layout_width="30dp" android:layout_height="30dp" android:layout_centerHorizontal="true" diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml index c579fae71..02691ab4b 100644 --- a/res/values-az/strings.xml +++ b/res/values-az/strings.xml @@ -26,7 +26,6 @@ <string name="author_label">Müəlif</string> <string name="language_label">Dil</string> <string name="podcast_settings_label">Parametrlər</string> - <string name="cover_label">Üz:</string> <string name="error_label">Xəta</string> <string name="error_msg_prefix">Xəta baş verdi:</string> <string name="refresh_label">Təzələ</string> @@ -128,8 +127,7 @@ <string name="action_forbidden_msg">Bu əməliyyat üçün AntennaPod\'un icazəsi yoxdur. AntennaPod\'un keçid tokenin ləğv olunması bunun səbəbi ola bilər. Yenə Flattra girin ya da podkastın səhifəsinə keçin.</string> <string name="access_revoked_title">Keçid ləğv olundu</string> <string name="access_revoked_info">AntennaPod\'un keçid tokeni uğurlu ləğv olundu.</string> - <string name="flattr_click_success">Flattrma uğurludur</string> - <string name="flattring_label">Flattrləmə</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">Plagin yüklə</string> <string name="no_playback_plugin_title">Plagin yüklü deyil</string> @@ -235,4 +233,5 @@ <string name="set_to_default_folder">Başlanğıc qovluqu seç</string> <!--Online feed view--> <string name="downloading_label">Yükləmə...</string> + <!--Content descriptions for image buttons--> </resources> diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 243b63772..94661466d 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">Autor</string> <string name="language_label">Llengua</string> <string name="podcast_settings_label">Configuració</string> - <string name="cover_label">Caràtula</string> + <string name="cover_label">Imatge</string> <string name="error_label">Error</string> <string name="error_msg_prefix">S\'ha produït un error:</string> <string name="refresh_label">Actualitza</string> @@ -140,8 +140,7 @@ <string name="action_forbidden_msg">AntennaPod no té permisos per executar aquesta acció. És possible que el testimoni d\'accés de Flattr per a AntennaPod hagi estat revocat. Podeu tornar-vos a autenticar amb el servei de Flattr, o podeu visitar el web del contingut directament.</string> <string name="access_revoked_title">L\'accés ha estat revocat</string> <string name="access_revoked_info">El testimoni d\'accés a Flattr de l\'AntennaPod s\'ha revocat correctament. Per completar el procés, heu de suprimir aquesta aplicació de la llista d\'aplicacions aprovades que trobareu a l\'apartat de configuració del compte de la plana web de Flattr.</string> - <string name="flattr_click_success">S\'ha compartit el contingut a través de Flattr</string> - <string name="flattring_label">S\'està compartint amb Flattr</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">Baixa el connector</string> <string name="no_playback_plugin_title">Connector no instal·lat</string> @@ -292,4 +291,5 @@ <string name="subscribe_label">Subscriu</string> <string name="subscribed_label">Subscrit</string> <string name="downloading_label">S\'està baixant...</string> + <!--Content descriptions for image buttons--> </resources> diff --git a/res/values-cs-rCZ/strings.xml b/res/values-cs-rCZ/strings.xml index 9d7881057..7958175bd 100644 --- a/res/values-cs-rCZ/strings.xml +++ b/res/values-cs-rCZ/strings.xml @@ -28,7 +28,6 @@ <string name="author_label">Autor</string> <string name="language_label">Jazyk</string> <string name="podcast_settings_label">Nastavení</string> - <string name="cover_label">Obal</string> <string name="error_label">Chyba</string> <string name="error_msg_prefix">Nastala chyba:</string> <string name="refresh_label">Obnovit</string> @@ -140,8 +139,7 @@ <string name="action_forbidden_msg">AntennaPod nemá oprávnění pro tuto akci. Důvodem může být revokování přístupového tokenu AntennaPodu k vašemu účtu. Přístup můžete obnovit nebo využít prohlížeče k návštěvě stránky zdroje.</string> <string name="access_revoked_title">Přístup revokován</string> <string name="access_revoked_info">Úspěšně revokován přístup AntennPodu k vašemu účtu. Pro dokončení tohoto procesu je ještě zapotřebí na stránkách flattru odebrat z vašeho účtu AntennaPod ze seznamu povolených aplikací.</string> - <string name="flattr_click_success">Úspěšně flattrováno!</string> - <string name="flattring_label">Flattruji</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">Stáhnout Plugin</string> <string name="no_playback_plugin_title">Plugin není nainstalován</string> @@ -292,4 +290,5 @@ <string name="subscribe_label">Odebírat</string> <string name="subscribed_label">Odebíráno</string> <string name="downloading_label">Stahuji...</string> + <!--Content descriptions for image buttons--> </resources> diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index 116ece995..a3e770f3d 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -24,7 +24,6 @@ <string name="cancel_label">Annuller</string> <string name="author_label">Forfatter</string> <string name="language_label">Sprog</string> - <string name="cover_label">Cover</string> <string name="error_label">Fejl</string> <string name="error_msg_prefix">En fejl er opstået:</string> <string name="refresh_label">Opdater</string> @@ -126,8 +125,7 @@ <string name="action_forbidden_msg">AntennaPod har ikke tilladelse til denne handling. Årsagen kunne være at adgangspoletten for AntennaPod til din konto er blevet tilbagekaldt. Du kan enten godkende den igen eller besøge websiden for mediet istedet.</string> <string name="access_revoked_title">Adgang tilbagekaldt</string> <string name="access_revoked_info">Du har succesfuldt tilbagekaldt AntennaPods adgangs polet til din konto. For at fuldføre processen skal du fjerne denne app fra listen af godkendte applikationer i din kontos indstillinger på flattr\'s hjemmeside.</string> - <string name="flattr_click_success">Det er lykkedes at flattr dette emne!</string> - <string name="flattring_label">Flattere</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">Hent Plugin</string> <string name="no_playback_plugin_title">Plugin er ikke installeret</string> @@ -230,4 +228,5 @@ <string name="folder_not_empty_dialog_msg">Mappen du har valgt er ikke tom. Medie downloads og andre filer vil blive placeret i denne mappe. Forsæt alligevel?</string> <string name="set_to_default_folder">Vælg standard mappe</string> <!--Online feed view--> + <!--Content descriptions for image buttons--> </resources> diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index e25b60fe4..8f1aabcff 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">Autor</string> <string name="language_label">Sprache</string> <string name="podcast_settings_label">Einstellungen</string> - <string name="cover_label">Cover</string> + <string name="cover_label">Bild</string> <string name="error_label">Fehler</string> <string name="error_msg_prefix">Ein Fehler ist aufgetreten:</string> <string name="refresh_label">Aktualisieren</string> @@ -140,8 +140,18 @@ <string name="action_forbidden_msg">AntennaPod besitzt keine Erlaubnis für diese Aktion. Der Grund dafür könnte sein, dass AntennaPods Zugangstoken aufgehoben worden ist. Du kannst dich entweder erneut authentifizieren oder die Flattr-Seite der Sache im Web besuchen.</string> <string name="access_revoked_title">Zugriff widerrufen</string> <string name="access_revoked_info">Du hast AntennaPod das Zugangstoken zu deinem Account entzogen. Um diesen Prozess abzuschließen, musst du diese Anwendung aus der Liste der zugelassenen Anwendungen in deinen Account Einstellungen auf der Flattr Webseite entfernen.</string> - <string name="flattr_click_success">Du hast erfolgreich diese Sache mit Flattr unterstützt!</string> - <string name="flattring_label">Flattre diese Sache</string> + <!--Flattr--> + <string name="flattr_click_success">Eine Sache wurde geflattrt!</string> + <string name="flattr_click_success_count">%d Sachen wurden geflattrt!</string> + <string name="flattr_click_success_queue">Geflattrt: %s</string> + <string name="flattr_click_failure_count">Flattrn von %d Sachen fehlgeschlagen!</string> + <string name="flattr_click_failure">Nicht geflattrt: %s</string> + <string name="flattr_click_enqueued">Sache wird später gelfattrt</string> + <string name="flattring_thing">Flattrt: %s</string> + <string name="flattring_label">AntennaPod flattrt</string> + <string name="flattrd_label">AntennaPod hat geflattrt</string> + <string name="flattrd_failed_label">AntennaPod Flattrn fehlgeschlagen</string> + <string name="flattr_retrieving_status">Rufe geflatterte Sachen ab</string> <!--Variable Speed--> <string name="download_plugin_label">Plugin herunterladen</string> <string name="no_playback_plugin_title">Plugin nicht installiert</string> @@ -176,6 +186,8 @@ <string name="pref_flattr_this_app_sum">Unterstütze die Entwicklung von AntennaPod mit Flattr. Danke!</string> <string name="pref_revokeAccess_title">Zugriff entziehen</string> <string name="pref_revokeAccess_sum">Entziehe dieser Anwendung die Zugriffserlaubnis für deinen Flattr Account.</string> + <string name="pref_auto_flattr_title">Automatisches Flattrn</string> + <string name="pref_auto_flattr_sum">Flattr Episoden, die zu 80 Prozent gespielt wurden.</string> <string name="pref_display_only_episodes_title">Nur Episoden anzeigen</string> <string name="pref_display_only_episodes_sum">Zeige nur Feed-Einträge mit Episoden an.</string> <string name="user_interface_label">Benutzeroberfläche</string> @@ -292,4 +304,21 @@ <string name="subscribe_label">Abonnieren</string> <string name="subscribed_label">Abonniert</string> <string name="downloading_label">Lade herunter...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Kapitel anzeigen</string> + <string name="show_shownotes_label">Sendungsnotizen anzeigen</string> + <string name="show_cover_label">Bild anzeigen</string> + <string name="rewind_label">Zurückspulen</string> + <string name="fast_forward_label">Vorspulen</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Nach oben navigieren</string> + <string name="butAction_label">Mehr Aktionen</string> + <string name="status_playing_label">Episode wird gerade abgespielt</string> + <string name="status_downloading_label">Episode wird gerade heruntergeladen</string> + <string name="status_downloaded_label">Episode ist heruntergeladen</string> + <string name="status_unread_label">Eintrag ist neu</string> + <string name="in_queue_label">Episode befindet sich inder Abspielliste</string> + <string name="new_episodes_count_label">Anzahl neuer Episoden</string> + <string name="in_progress_episodes_count_label">Anzahl der Episoden, die du angefangen hast zu hören</string> </resources> diff --git a/res/values-es-rES/strings.xml b/res/values-es-rES/strings.xml index 541019d0b..721faa65e 100644 --- a/res/values-es-rES/strings.xml +++ b/res/values-es-rES/strings.xml @@ -24,7 +24,6 @@ <string name="cancel_label">Cancelar</string> <string name="author_label">Autor</string> <string name="language_label">Idioma</string> - <string name="cover_label">Carátula</string> <string name="error_label">Error</string> <string name="error_msg_prefix">Ha ocurrido un error:</string> <string name="refresh_label">Actualizar</string> @@ -122,8 +121,7 @@ <string name="action_forbidden_msg">AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa.</string> <string name="access_revoked_title">Acceso revocado</string> <string name="access_revoked_info">Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr.</string> - <string name="flattr_click_success">Ha valorado esto en Flattr.</string> - <string name="flattring_label">Valoración en Flattr</string> + <!--Flattr--> <!--Variable Speed--> <!--Empty list labels--> <string name="no_items_label">Esta lista no tiene elementos.</string> @@ -218,4 +216,5 @@ <string name="folder_not_empty_dialog_msg">La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente?</string> <string name="set_to_default_folder">Elegir carpeta predeterminada</string> <!--Online feed view--> + <!--Content descriptions for image buttons--> </resources> diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index c806fdea8..4d8dbd85b 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">Autor</string> <string name="language_label">Idioma</string> <string name="podcast_settings_label">Ajustes</string> - <string name="cover_label">Carátula</string> + <string name="cover_label">Imagen</string> <string name="error_label">Error</string> <string name="error_msg_prefix">Ha ocurrido un error:</string> <string name="refresh_label">Actualizar</string> @@ -140,8 +140,18 @@ <string name="action_forbidden_msg">AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa.</string> <string name="access_revoked_title">Acceso revocado</string> <string name="access_revoked_info">Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr.</string> - <string name="flattr_click_success">Ha valorado esto en Flattr.</string> - <string name="flattring_label">Valoración en Flattr</string> + <!--Flattr--> + <string name="flattr_click_success">¡Flattr una cosa!</string> + <string name="flattr_click_success_count">¡Flattr %d cosas!</string> + <string name="flattr_click_success_queue">Flattr: %s.</string> + <string name="flattr_click_failure_count">¡Falló Flattr de %d cosas!</string> + <string name="flattr_click_failure">No se hizo Flattr: %s.</string> + <string name="flattr_click_enqueued">Se hará Flattr de esta cosa más tarde</string> + <string name="flattring_thing">Haciendo Flattr de %s</string> + <string name="flattring_label">AntennaPod haciendo Flattr</string> + <string name="flattrd_label">AntennaPod hizo Flattr</string> + <string name="flattrd_failed_label">AntennaPod Flattr falló</string> + <string name="flattr_retrieving_status">Obteniendo lista de Flattr</string> <!--Variable Speed--> <string name="download_plugin_label">Descargar complemento</string> <string name="no_playback_plugin_title">Complemento no instalado</string> @@ -176,6 +186,8 @@ <string name="pref_flattr_this_app_sum">Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias!</string> <string name="pref_revokeAccess_title">Revocar el acceso</string> <string name="pref_revokeAccess_sum">Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr.</string> + <string name="pref_auto_flattr_title">Uso de Flattr automático</string> + <string name="pref_auto_flattr_sum">Hacer Flattr al reproducir el 80 por ciento de cada episodio</string> <string name="pref_display_only_episodes_title">Mostrar solo episodios</string> <string name="pref_display_only_episodes_sum">Mostrar solo elementos que contengan un episodio.</string> <string name="user_interface_label">Interfaz de usuario</string> @@ -292,4 +304,21 @@ <string name="subscribe_label">Suscribirse</string> <string name="subscribed_label">Suscrito</string> <string name="downloading_label">Descargando…</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Mostrar capítulos</string> + <string name="show_shownotes_label">Mostrar notas del programa</string> + <string name="show_cover_label">Mostrar imagen</string> + <string name="rewind_label">Rebobinar</string> + <string name="fast_forward_label">Avance rápido</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Vídeo</string> + <string name="navigate_upwards_label">Navegar hacia arriba</string> + <string name="butAction_label">Más acciones</string> + <string name="status_playing_label">El episodio se está reproduciendo</string> + <string name="status_downloading_label">El episodio se está descargando</string> + <string name="status_downloaded_label">El episodio está descargado</string> + <string name="status_unread_label">El elemento es nuevo</string> + <string name="in_queue_label">El episodio está en la cola</string> + <string name="new_episodes_count_label">Cantidad de episodios nuevos</string> + <string name="in_progress_episodes_count_label">Cantidad de episodios que ha comenzado a escuchar</string> </resources> diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index c8bd652f3..8a8492b67 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">Auteur</string> <string name="language_label">Langue</string> <string name="podcast_settings_label">Préférences</string> - <string name="cover_label">Couverture</string> + <string name="cover_label">Image</string> <string name="error_label">Erreur</string> <string name="error_msg_prefix">Une erreur a eu lieu :</string> <string name="refresh_label">Rafraîchir</string> @@ -140,8 +140,18 @@ <string name="action_forbidden_msg">AntennaPod n\'a pas la permission pour cette action. Il est possible que l\'accès à votre compte depuis AntennaPod ait été révoqué. Vous pouvez vous authentifier à nouveau, ou bien visiter le site à flattr directement.</string> <string name="access_revoked_title">Accès révoqué</string> <string name="access_revoked_info">Vous avez révoqué le jeton d\'accès d\'AntennaPod à votre compte. Pour terminer cette opération, vous devez retirer AntennaPod de la liste des applications autorisées sur le site web de Flattr.</string> - <string name="flattr_click_success">Flattr réussi !</string> - <string name="flattring_label">Flattr en cours</string> + <!--Flattr--> + <string name="flattr_click_success">Une chose de Flattré !</string> + <string name="flattr_click_success_count">%d choses de Flattré !</string> + <string name="flattr_click_success_queue">Flattré : %s.</string> + <string name="flattr_click_failure_count">Impossible de Flattrer %d choses !</string> + <string name="flattr_click_failure">Non Flattré : %s.</string> + <string name="flattr_click_enqueued">Cette chose sera Flattré plus tard</string> + <string name="flattring_thing">En train de Flattrer %s</string> + <string name="flattring_label">AntennaPod est en train de Flattrer</string> + <string name="flattrd_label">AntennaPod a Flattré</string> + <string name="flattrd_failed_label">Flattr d\'AntennaPod a échoué</string> + <string name="flattr_retrieving_status">Obtention de la liste des choses Flattrées</string> <!--Variable Speed--> <string name="download_plugin_label">Télécharger une extension</string> <string name="no_playback_plugin_title">Extension non installée</string> @@ -176,6 +186,8 @@ <string name="pref_flattr_this_app_sum">Encouragez le développement d\'AntennaPod grâce à Flattr. Merci !</string> <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">Flattrer les épisodes dont 80 pour-cents ont été joués.</string> <string name="pref_display_only_episodes_title">N\'afficher que les épisodes</string> <string name="pref_display_only_episodes_sum">N\'afficher que les flux qui ont au moins un épisode.</string> <string name="user_interface_label">Interface utilisateur</string> @@ -292,4 +304,21 @@ <string name="subscribe_label">S\'abonner</string> <string name="subscribed_label">Abonné</string> <string name="downloading_label">Téléchargement en cours</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Afficher chapitres</string> + <string name="show_shownotes_label">Afficher notes d\'épisode</string> + <string name="show_cover_label">Afficher image</string> + <string name="rewind_label">Retour en arrière</string> + <string name="fast_forward_label">Avance rapide</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Vidéo</string> + <string name="navigate_upwards_label">Naviguer vers le haut</string> + <string name="butAction_label">Plus d\'actions</string> + <string name="status_playing_label">L\'épisode est en train d\'être joué</string> + <string name="status_downloading_label">L\'épisode est en train d\'être téléchargé</string> + <string name="status_downloaded_label">L\'épisode a été téléchargé</string> + <string name="status_unread_label">L\'élément est nouveau</string> + <string name="in_queue_label">L\'épisode est dans la liste</string> + <string name="new_episodes_count_label">Nombre de nouveaux épisodes</string> + <string name="in_progress_episodes_count_label">Nombre d\'épisodes que vous avez commencé à écouter</string> </resources> diff --git a/res/values-hi-rIN/strings.xml b/res/values-hi-rIN/strings.xml index 0ce8dce4c..faaed3dcd 100644 --- a/res/values-hi-rIN/strings.xml +++ b/res/values-hi-rIN/strings.xml @@ -28,7 +28,6 @@ <string name="author_label">\tनिर्माता</string> <string name="language_label">भाषा</string> <string name="podcast_settings_label">सेटिंग्स</string> - <string name="cover_label">आवरण</string> <string name="error_label">त्रुटि</string> <string name="error_msg_prefix">एक त्रुटि हो गई:</string> <string name="refresh_label">ताज़ा करें</string> @@ -140,8 +139,7 @@ <string name="action_forbidden_msg">AntennaPod को इस कार्रवाई के लिए अनुमति नहीं है.इस के लिए कारण हो सकता है की आपके खाते में AntennaPod की पहुँच टोकन को निरस्त किया गया है.आप या तो फिर से प्रमाणित कर सकते हैं या बजाय किसी बात के वेबसाइट पर जा सकते हैं.</string> <string name="access_revoked_title">प्रवेश निरस्त किया</string> <string name="access_revoked_info">आपने सफलतापूर्वक अपने खाते में AntennaPod पहुँच टोकन निरस्त कर दिया है. इस प्रक्रिया को पूरा करने के लिए, आपको flattr वेबसाइट पर अपने खाते की सेटिंग्स में अनुमोदित आवेदनों की सूची से इस एप्लिकेशन को हटाना होगा.</string> - <string name="flattr_click_success">सफलतापूर्वक इस बात flattred कर दिया गया है!</string> - <string name="flattring_label">Flattr करें</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">प्लगइन डाउनलोड करें</string> <string name="no_playback_plugin_title">प्लगइन स्थापित नहीं हुआ</string> @@ -292,4 +290,5 @@ <string name="subscribe_label">सदस्यता लें</string> <string name="subscribed_label">सदस्यता ली गई</string> <string name="downloading_label">डाउनलोड कर रहा है ...</string> + <!--Content descriptions for image buttons--> </resources> diff --git a/res/values-it-rIT/strings.xml b/res/values-it-rIT/strings.xml index aae39d327..cd71af90c 100644 --- a/res/values-it-rIT/strings.xml +++ b/res/values-it-rIT/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">Autore</string> <string name="language_label">Lingua</string> <string name="podcast_settings_label">Impostazioni</string> - <string name="cover_label">Copertina</string> + <string name="cover_label">Immagine</string> <string name="error_label">Errore</string> <string name="error_msg_prefix">Un errore è stato rilevato:</string> <string name="refresh_label">Aggiorna</string> @@ -140,8 +140,8 @@ <string name="action_forbidden_msg">AntennaPod non ha il permesso di effettuare questa azione. La ragione potrebbe essere che il token di accesso di AntennaPod al tuo account è stato revocato. Puoi eseguire la re-autenticazione o altrimenti visitare il sito web.</string> <string name="access_revoked_title">Accesso revocato</string> <string name="access_revoked_info">Hai revocato l\'accesso di AntennaPod al tuo account. Al fine di completare il processo devi rimuovere l\'app dalla lista delle applicazioni autorizzare nelle impostazioni del tuo account sul sito di flattr.</string> - <string name="flattr_click_success">Flattr eseguito con successo!</string> - <string name="flattring_label">Flattr in corso</string> + <!--Flattr--> + <string name="flattring_label">AntennaPod sta eseguendo Flattr</string> <!--Variable Speed--> <string name="download_plugin_label">Scarica Plugin</string> <string name="no_playback_plugin_title">Plugin non installato</string> @@ -292,4 +292,20 @@ <string name="subscribe_label">Abbonati</string> <string name="subscribed_label">Abbonato</string> <string name="downloading_label">Download in corso...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Mostra i capitoli</string> + <string name="show_shownotes_label">Mostra le note dell\'episodio</string> + <string name="show_cover_label">Mosta l\'immagine</string> + <string name="rewind_label">Riavvolgi</string> + <string name="fast_forward_label">Avanti veloce</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Naviga su</string> + <string name="butAction_label">Più azioni</string> + <string name="status_playing_label">L\'episodio è in corso di ripoduzione</string> + <string name="status_downloading_label">L\'episodio sta per essere scaricato</string> + <string name="status_downloaded_label">L\'episodio è stato scaricato</string> + <string name="status_unread_label">L\'oggetto è nuovo</string> + <string name="in_queue_label">L\'episodio è in coda</string> + <string name="new_episodes_count_label">Numero dei nuovi episodi</string> </resources> diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 6f26c5d1f..68537d1d8 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -8,10 +8,13 @@ <string name="new_label">Nieuw</string> <string name="waiting_list_label">Wachtlijst</string> <string name="settings_label">Instellingen</string> + <string name="add_new_feed_label">Podcast toevoegen</string> <string name="downloads_label">Downloads</string> <string name="cancel_download_label">Annuleer download</string> <string name="download_log_label">Download log</string> <string name="playback_history_label">Afspeelgeschiedenis</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> <!--Webview actions--> <string name="open_in_browser_label">In de browser openen</string> <string name="copy_url_label">URL kopieren</string> @@ -24,13 +27,15 @@ <string name="cancel_label">Annuleer</string> <string name="author_label">Auteur</string> <string name="language_label">Taal</string> - <string name="cover_label">Cover</string> + <string name="podcast_settings_label">Instellingen</string> + <string name="cover_label">Beeld</string> <string name="error_label">Fout</string> <string name="error_msg_prefix">Er is een fout opgetreden:</string> <string name="refresh_label">Verversen</string> <string name="external_storage_error_msg">Geen externe opslag beschikbaar. Zorg ervoor dat de externe opslag gemonteerd is, zodat de app goed kan werken.</string> <string name="chapters_label">Hoofdstukken</string> <string name="shownotes_label">Shownotes</string> + <string name="description_label">Beschrijving</string> <string name="most_recent_prefix">Meest recent episode:\u0020</string> <string name="episodes_suffix">\u0020episodes</string> <string name="published_prefix">Gepubliceerd:\u0020</string> @@ -39,9 +44,14 @@ <string name="processing_label">Aan het verwerken</string> <string name="loading_label">Laden...</string> <string name="image_of_prefix">Beeld van:\u0020</string> + <string name="save_username_password_label">Gebruikersnaam en wachtwoord opslaan</string> <string name="close_label">Sluiten</string> + <string name="retry_label">Opnieuw proberen</string> + <string name="auto_download_label">Voor het automatisch downloaden beschouwen</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Feed URL</string> + <string name="txtvfeedurl_label">Podcast toevoegen bij URL</string> + <string name="podcastdirectories_label">Podcast lijsten</string> <!--Actions on feeds--> <string name="mark_all_read_label">Alles als gelezen markeren</string> <string name="show_info_label">Toon informatie</string> @@ -49,6 +59,7 @@ <string name="share_link_label">Website link delen</string> <string name="share_source_label">Feed link delen</string> <string name="feed_delete_confirmation_msg">Bevestig dat u deze feed en ALLE episodes van deze feed die u hebt gedownload wilt verwijderen.</string> + <string name="feed_remover_msg">Feed verwijderen</string> <!--actions on feeditems--> <string name="download_label">Download</string> <string name="play_label">Spelen</string> @@ -84,6 +95,7 @@ <string name="download_error_malformed_url">Misvormde URL</string> <string name="download_error_io_error">IO fout</string> <string name="download_error_request_error">Fout in de aanvraag</string> + <string name="download_error_db_access">Databasetoegangsfout</string> <string name="downloads_left">Nog \u0020 downloads</string> <string name="download_notification_title">Podcast gegevens aan het downloaden</string> <string name="download_report_content">%1$d downloads geslaagd, %2$d mislukt</string> @@ -113,6 +125,8 @@ <string name="organize_queue_label">Wachtrij organiseren</string> <string name="undo">Ongedaan maken</string> <string name="removed_from_queue">Item verwijderd</string> + <string name="move_to_top_label">Naar boven verplaatsen</string> + <string name="move_to_bottom_label">Naar beneden verplaatsen</string> <!--Flattr--> <string name="flattr_auth_label">Flattr inloggen</string> <string name="flattr_auth_explanation">Druk op onderstaande knop om het verificatieproces te starten. U wordt doorgestuurd naar de Flattr inlogscherm in uw browser en wordt gevraagd om toestemming aan AntennaPod te geven om dingen te Flattr\'en. Nadat u toestemming hebt gegeven, keert u automatisch terug naar dit scherm.</string> @@ -126,8 +140,7 @@ <string name="action_forbidden_msg">AntennaPod heeft geen toestemming voor deze actie. De reden hiervoor zou kunnen zijn dat de toegang token van AntennaPod voor uw account ingetrokken is. U kunt opnieuw authenticeren, of de website van het ding bezoeken.</string> <string name="access_revoked_title">Toegang ingetrokken</string> <string name="access_revoked_info">U heeft met succes het toegangstoken van AntennaPod tot uw account ingetrokken. Om het proces te voltooien, moet u deze app uit de lijst van goedgekeurde applicaties in uw accountinstellingen op de Flattr website verwijderen.</string> - <string name="flattr_click_success">Dit ding met succes geflattr\'d!</string> - <string name="flattring_label">Aan het flattr\'en</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">Plugin downloaden</string> <string name="no_playback_plugin_title">Plugin niet geinstalleerd</string> @@ -140,6 +153,8 @@ <string name="other_pref">Overig</string> <string name="about_pref">Over AntennaPod</string> <string name="queue_label">Wachtrij</string> + <string name="services_label">Services</string> + <string name="flattr_label">Flattr</string> <string name="pref_pauseOnHeadsetDisconnect_sum">Afspelen pauzeren wanneer de hoofdtelefoon wordt losgekoppeld</string> <string name="pref_followQueue_sum">Volgende wachtrij item afspelen als de episode voltooid is</string> <string name="playback_pref">Afspelen</string> @@ -168,7 +183,7 @@ <string name="pref_automatic_download_title">Automatisch downloaden</string> <string name="pref_automatic_download_sum">Configureer het automatisch downloaden van episoden.</string> <string name="pref_autodl_wifi_filter_title">Wi-Fi filter inschakelen</string> - <string name="pref_autodl_wifi_filter_sum">Automatisch download alleen voor geselecteerde Wi-Fi-netwerken toestaan.</string> + <string name="pref_autodl_wifi_filter_sum">Automatisch downloaden alleen toestaan voor geselecteerde Wi-Fi-netwerken.</string> <string name="pref_episode_cache_title">Episode cache</string> <string name="pref_theme_title_light">Licht</string> <string name="pref_theme_title_dark">Donker</string> @@ -176,8 +191,16 @@ <string name="pref_update_interval_hours_plural">uren</string> <string name="pref_update_interval_hours_singular">uur</string> <string name="pref_update_interval_hours_manual">Handmatig</string> + <string name="pref_gpodnet_authenticate_title">Log in</string> + <string name="pref_gpodnet_authenticate_sum">Log met je gpodder.net account in om je abonnementen te synchroniseren.</string> + <string name="pref_gpodnet_logout_title">Log uit</string> + <string name="pref_gpodnet_logout_toast">Uitlog was succesvol</string> + <string name="pref_gpodnet_setlogin_information_title">Aanmeldingsgegevens wijzigen</string> + <string name="pref_gpodnet_setlogin_information_sum">Wijzig de aanmeldingsgegevens van je gpodder.net account.</string> <string name="pref_playback_speed_title">Afspeelsnelheden</string> <string name="pref_playback_speed_sum">Pas de beschikbare snelheden aan voor de variabele audio afspeelsnelheid</string> + <string name="pref_gpodnet_sethostname_title">Definieer hostname</string> + <string name="pref_gpodnet_sethostname_use_default_host">Gebruik standaard host</string> <!--Search--> <string name="search_hint">Feeds of episodes zoeken</string> <string name="found_in_shownotes_label">Gevonden in de shownotes</string> @@ -189,6 +212,7 @@ <string name="search_label">Zoeken</string> <string name="found_in_title_label">Gevonden in de titel</string> <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Met OPML-bestanden kan je podcasts van de ene naar de andere podcatcher verplaatsen.</string> <string name="opml_import_explanation">Om een OPML-bestand te importeren moet je het in de volgende map zetten en op onderstaande knop drukken.</string> <string name="start_import_label">Start importeren</string> <string name="opml_import_label">OPML import</string> @@ -219,8 +243,36 @@ <string name="miro_search_hint">Zoek Miro gids</string> <string name="popular_label">Populair</string> <string name="best_rating_label">Beste beoordeling</string> + <string name="add_feed_label">Podcast toevoegen</string> <string name="miro_feed_added">Feed wordt toegevoegd</string> <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORIEËN</string> + <string name="gpodnet_toplist_header">TOP PODCASTS</string> + <string name="gpodnet_suggestions_header">SUGGESTIES</string> + <string name="gpodnet_search_hint">Zoek gpodder.net</string> + <string name="gpodnetauth_login_title">Log in</string> + <string name="gpodnetauth_login_descr">Welkom op de gpodder.net login proces. Eerst, typ je login gegevens:</string> + <string name="gpodnetauth_login_butLabel">Log in</string> + <string name="gpodnetauth_login_register">Als je nog geen account hebt, kun je er hier een aanmaken:\n https://gpodder.net/register/</string> + <string name="username_label">Gebruikersnaam</string> + <string name="password_label">Wachtwoord</string> + <string name="gpodnetauth_device_title">Apparaatselectie</string> + <string name="gpodnetauth_device_descr">Maak een nieuw apparaat aan om voor je gpodder.net account te gebruiken of kies een bestaande:</string> + <string name="gpodnetauth_device_deviceID">Device ID:\u0020</string> + <string name="gpodnetauth_device_caption">Titel</string> + <string name="gpodnetauth_device_butCreateNewDevice">Maak een nieuw apparaat aan</string> + <string name="gpodnetauth_device_chooseExistingDevice">Kies een bestaand apparaat:</string> + <string name="gpodnetauth_device_errorEmpty">Apparaat ID mag niet leeg zijn</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Apparaat ID al in gebruik</string> + <string name="gpodnetauth_device_butChoose">Kies</string> + <string name="gpodnetauth_finish_title">Login succesvol</string> + <string name="gpodnetauth_finish_descr">Gefeliciteerd! Jou gpodder.net account is nu verbonden met je apparaat. AntennaPod zal voortaan abonnementen op je apparaat automatisch met je gpodder.net account synchroniseren.</string> + <string name="gpodnetauth_finish_butsyncnow">Synchronisatie nu starten</string> + <string name="gpodnetauth_finish_butgomainscreen">Terug naar hoofdscherm</string> + <string name="gpodnetsync_auth_error_title">gpodder.net authenticatie fout</string> + <string name="gpodnetsync_auth_error_descr">Ongeldig gebruikersnaam of wachtwoord</string> + <string name="gpodnetsync_error_title">gpodder.net synchronisatie fout</string> + <string name="gpodnetsync_error_descr">Er is een fout opgetreden tijdens het synchroniseren:\u0020</string> <!--Directory chooser--> <string name="selected_folder_label">Geselecteerde map:</string> <string name="create_folder_label">Map aanmaken</string> @@ -233,5 +285,14 @@ <string name="folder_not_empty_dialog_title">Map is niet leeg</string> <string name="folder_not_empty_dialog_msg">De map die je hebt gekozen is niet leeg. Media downloads en andere bestanden zullen rechtstreeks in deze map geplaatst worden. Toch doorgaan?</string> <string name="set_to_default_folder">Kies default map</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Het afspelen onderbreken in plaats van het volume te verlagen wanneer er een andere app geluiden af wilt spelen</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pauze voor onderbrekingen</string> <!--Online feed view--> + <string name="subscribe_label">Abonneren</string> + <string name="subscribed_label">Geabonneerd</string> + <string name="downloading_label">Aan het downloaden</string> + <!--Content descriptions for image buttons--> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="butAction_label">Meer acties</string> </resources> diff --git a/res/values-pl-rPL/strings.xml b/res/values-pl-rPL/strings.xml new file mode 100644 index 000000000..34096a769 --- /dev/null +++ b/res/values-pl-rPL/strings.xml @@ -0,0 +1,307 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Kanały</string> + <string name="podcasts_label">PODCASTY</string> + <string name="episodes_label">ODCINKI</string> + <string name="new_label">Nowy</string> + <string name="waiting_list_label">Lista oczekujących</string> + <string name="settings_label">Ustawienia</string> + <string name="add_new_feed_label">Dodaj podcast</string> + <string name="downloads_label">Pobrane</string> + <string name="cancel_download_label">Anuluj pobieranie</string> + <string name="download_log_label">Dziennik pobierania</string> + <string name="playback_history_label">Historia odtwarzania</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--Webview actions--> + <string name="open_in_browser_label">Otwórz w przeglądarce</string> + <string name="copy_url_label">Kopiuj adres</string> + <string name="share_url_label">Udostępnij adres</string> + <string name="copied_url_msg">Skopiowano adres do schowka.</string> + <!--Playback history--> + <string name="clear_history_label">Wyczyść historię</string> + <!--Other--> + <string name="confirm_label">Potwierdź </string> + <string name="cancel_label">Anuluj</string> + <string name="author_label">Autor</string> + <string name="language_label">Język</string> + <string name="podcast_settings_label">Ustawienia</string> + <string name="error_label">Błąd</string> + <string name="error_msg_prefix">Wystąpił błąd:</string> + <string name="refresh_label">Odśwież</string> + <string name="external_storage_error_msg">Brak zewnętrznej pamięci. Sprawdź czy jest ona podłączona żeby aplikacja mogła pracować poprawnie.</string> + <string name="chapters_label">Rozdziały</string> + <string name="shownotes_label">Pokaż notatki</string> + <string name="description_label">Opis</string> + <string name="most_recent_prefix">Najnowszy odcinek:\u0020</string> + <string name="episodes_suffix">:\u0020odcinków</string> + <string name="published_prefix">Opublikowane:\u0020</string> + <string name="length_prefix">Długość:\u0020</string> + <string name="size_prefix">Rozmiar:\u0020</string> + <string name="processing_label">Przetwarzanie</string> + <string name="loading_label">Ładowanie...</string> + <string name="image_of_prefix">Obraz z:\u0020</string> + <string name="save_username_password_label">Zapisz nazwę użytkownika i hasło</string> + <string name="close_label">Zamknij</string> + <string name="retry_label">Spróbuj ponownie</string> + <string name="auto_download_label">Dołącz do automatycznego pobierania</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Adres kanału</string> + <string name="txtvfeedurl_label">Dodaj podcast przez adres</string> + <string name="podcastdirectories_label">Katalogi podcastów</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Oznacz wszystkie jako przeczytane</string> + <string name="show_info_label">Pokaż informacje</string> + <string name="remove_feed_label">Usuń kanał</string> + <string name="share_link_label">Udostępnij stronę</string> + <string name="share_source_label">Udostępnij kanał</string> + <string name="feed_delete_confirmation_msg">Potwierdź chęć usunięcia tego kanału wraz ze WSZYSTKIMI odcinkami, które zostały pobrane.</string> + <string name="feed_remover_msg">Usuwanie kanału</string> + <!--actions on feeditems--> + <string name="download_label">Pobierz</string> + <string name="play_label">Odtwórz</string> + <string name="pause_label">Pauza</string> + <string name="stream_label">Strumień</string> + <string name="remove_label">Usuń</string> + <string name="mark_read_label">Oznacz jako przeczytane</string> + <string name="mark_unread_label">Oznacz jako nieprzeczytane</string> + <string name="add_to_queue_label">Dodaj do kolejki</string> + <string name="remove_from_queue_label">Usuń z kolejki</string> + <string name="visit_website_label">Odwiedź stronę</string> + <string name="support_label">Wspomóż na Flattr</string> + <string name="enqueue_all_new">Dodaj wszystko do kolejki</string> + <string name="download_all">Pobierz wszystkie</string> + <string name="skip_episode_label">Pomiń odcinek</string> + <!--Download messages and labels--> + <string name="download_successful">Pobieranie ukończone</string> + <string name="download_failed">Pobieranie nieudane</string> + <string name="download_pending">Pobieranie w toku</string> + <string name="download_running">Pobieram</string> + <string name="download_error_device_not_found">Nie znaleziono urządzenia docelowego</string> + <string name="download_error_insufficient_space">Nie wystarczająca ilość pamięci</string> + <string name="download_error_file_error">Błąd pliku</string> + <string name="download_error_http_data_error">Błąd danych HTTP</string> + <string name="download_error_error_unknown">Nieznany błąd</string> + <string name="download_error_parser_exception">Wyjątek parsera</string> + <string name="download_error_unsupported_type">Nieobsługiwany typ kanału</string> + <string name="download_error_connection_error">Błąd połączenia</string> + <string name="download_error_unknown_host">Nieznany host</string> + <string name="cancel_all_downloads_label">Anuluj wszystkie pobierania</string> + <string name="download_cancelled_msg">Pobieranie anulowane</string> + <string name="download_report_title">Pobieranie ukończone</string> + <string name="download_error_malformed_url">Niepoprawny adres</string> + <string name="download_error_io_error">Błąd wejścia/wyjścia</string> + <string name="download_error_request_error">Błąd żądania</string> + <string name="download_error_db_access">Błąd dostępu do bazy danych</string> + <string name="downloads_left">:\u0020pobrań pozostało</string> + <string name="download_notification_title">Pobieranie danych podcastu</string> + <string name="download_report_content">%1$d pobierania poprawne, %2$d nieudane</string> + <string name="download_log_title_unknown">Nieznany tytuł</string> + <string name="download_type_feed">Kanał</string> + <string name="download_type_media">Plik multimedialny</string> + <string name="download_type_image">Obraz</string> + <string name="download_request_error_dialog_message_prefix">Wystąpił błąd przy próbie pobierania:\u0020</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Błąd!</string> + <string name="player_stopped_msg">Żadne media nie odtwarzane </string> + <string name="player_preparing_msg">Przygotowuję</string> + <string name="player_ready_msg">Gotowe</string> + <string name="player_seeking_msg">Szukam</string> + <string name="playback_error_server_died">Serwer zdechł</string> + <string name="playback_error_unknown">Nieznany błąd</string> + <string name="no_media_playing_label">Żadne media nie odtwarzane </string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buferowanie</string> + <string name="playbackservice_notification_title">Odtwarzenie podcastu </string> + <string name="playbackservice_notification_content">Więcej informacji</string> + <!--Navigation--> + <string name="show_download_log">Pokaż log</string> + <string name="show_player_label">Pokaż odtwarzacz</string> + <!--Queue operations--> + <string name="clear_queue_label">Wyczyść kolejkę</string> + <string name="organize_queue_label">Organizuj kolejkę</string> + <string name="undo">Cofnij</string> + <string name="removed_from_queue">Element usunięty</string> + <string name="move_to_top_label">Przesuń na górę</string> + <string name="move_to_bottom_label">Przesuń na dół</string> + <!--Flattr--> + <string name="flattr_auth_label">Logowanie do Flattr</string> + <string name="flattr_auth_explanation">Naciśnij przycisk poniżej by zacząć proces autoryzacji. Zostaniesz przekierowany na stronę logowanie do flattr w przeglądarce i zostaniesz poproszony o przyznanie zezwolenia AntennaPod-owi na flattr-owanie. Po daniu zezwolenia powrócisz do tej strony automatycznie.</string> + <string name="authenticate_label">Autoryzacja</string> + <string name="return_home_label">Wróć do ekranu głównego</string> + <string name="flattr_auth_success">Autoryzacja się powiodła. Możesz teraz używać flattr w aplikacji.</string> + <string name="no_flattr_token_title">Nie znaleziono tokenu Flattr</string> + <string name="no_flattr_token_msg">Twoje konto Flattr wydaje się nie być podłączone do AntennaPod. Możesz połączyć konto do AntennaPod by przez program flattr-ować lub możesz odwiedzić stronę wątku by zrobić to tam.</string> + <string name="authenticate_now_label">Autoryzuj</string> + <string name="action_forbidden_title">Akcja zabroniona</string> + <string name="action_forbidden_msg">AntennaPod nie ma zezwolenia na tą akcję. Powodem może być fakt iż dostęp dla AntennaPod do Twojego konta został cofnięty. Możesz ponownie autoryzować aplikację lub odwiedzić stronę. </string> + <string name="access_revoked_title">Anulowano dostęp</string> + <string name="access_revoked_info">Odwołałeś dostęp AntennaPod do swojego konta. W celu zakończenia procesu musisz usunąć aplikację z listy aplikacji dozwolonych na koncie Flattr.</string> + <!--Flattr--> + <!--Variable Speed--> + <string name="download_plugin_label">Pobierz wtyczkę</string> + <string name="no_playback_plugin_title">Wtyczka nie zainstalowana</string> + <string name="no_playback_plugin_msg">Do odtwarzania ze zmienną prędkością jest potrzebna biblioteka innej firmy. \n\nDotknij przycisku \"Pobierz wtyczkę\", aby pobrać darmową wtyczkę ze sklepu\n\nWszelkie znalezione za pomocą tej wtyczki problemy nie są odpowiedzialnością AntennaPod i należy zgłosić się do właściciela plugin.</string> + <string name="set_playback_speed_label">Prędkość odtwarzania</string> + <!--Empty list labels--> + <string name="no_items_label">Brak elementów na tej liście.</string> + <string name="no_feeds_label">Nie subskrybowałeś jeszcze żadnego kanału.</string> + <!--Preferences--> + <string name="other_pref">Inne</string> + <string name="about_pref">O...</string> + <string name="queue_label">Kolejka</string> + <string name="services_label">Usługi</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Wstrzymaj odtwarzanie kiedy słuchawki zostaną odłączone</string> + <string name="pref_followQueue_sum">Przeskocz do następnego elementu kolejki po zakończeniu odtwarzania</string> + <string name="playback_pref">Odtwarzanie</string> + <string name="network_pref">Sieć</string> + <string name="pref_autoUpdateIntervall_title">Częstość aktualizacji</string> + <string name="pref_autoUpdateIntervall_sum">Określ częstotliwość automatycznego odświeżania lub je wyłącz</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Pobieraj pliki tylko przez WiFi</string> + <string name="pref_followQueue_title">Odtwarzanie ciągłe</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi media pobrane</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Słuchawki odłączone</string> + <string name="pref_mobileUpdate_title">Aktualizacje mobilne</string> + <string name="pref_mobileUpdate_sum">Zezwól na aktualizacje poprzez sieć komórkową</string> + <string name="refreshing_label">Odświeżanie</string> + <string name="flattr_settings_label">Ustawienia Flattr</string> + <string name="pref_flattr_auth_title">Logowanie do Flattr</string> + <string name="pref_flattr_auth_sum">Zaloguj się do konta Flattr aby wspierać twórców bezpośrednio z aplikacji.</string> + <string name="pref_flattr_this_app_title">Wesprzyj aplikację na Flattr</string> + <string name="pref_flattr_this_app_sum">Wesprzyj twórcę AntennaPod przez Flattr. Dzięki!</string> + <string name="pref_revokeAccess_title">Anuluj dostęp</string> + <string name="pref_revokeAccess_sum">Anuluj dostęp tej aplikacji do konta Flattr </string> + <string name="pref_auto_flattr_title">Automatyczne wsparcie na Flattr</string> + <string name="pref_display_only_episodes_title">Wyświetlaj tylko odcinki</string> + <string name="pref_display_only_episodes_sum">Wyświetlaj tylko pozycje, które mają również odcinki.</string> + <string name="user_interface_label">Interfejs użytkownika</string> + <string name="pref_set_theme_title">Wybierz motyw</string> + <string name="pref_set_theme_sum">Zmień wygląd AntennaPod.</string> + <string name="pref_automatic_download_title">Automatyczne pobieranie</string> + <string name="pref_automatic_download_sum">Skonfiguruj automatyczne pobieranie odcinków.</string> + <string name="pref_autodl_wifi_filter_title">Włącz filtr Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Zezwól na automatyczne pobieranie tylko dla określonych sieci Wi-Fi.</string> + <string name="pref_episode_cache_title">Pamięć podręczna odcinków</string> + <string name="pref_theme_title_light">Jasny</string> + <string name="pref_theme_title_dark">Ciemny</string> + <string name="pref_episode_cache_unlimited">Nielimitowane</string> + <string name="pref_update_interval_hours_plural">godziny</string> + <string name="pref_update_interval_hours_singular">godzina</string> + <string name="pref_update_interval_hours_manual">Instrukcja</string> + <string name="pref_gpodnet_authenticate_title">Zaloguj</string> + <string name="pref_gpodnet_authenticate_sum">Zaloguj się swoim kontem na gpodder.net w celu synchronizacji Twoich subskrypcji.</string> + <string name="pref_gpodnet_logout_title">Wyloguj</string> + <string name="pref_gpodnet_logout_toast">Wylogowanie się powiodło</string> + <string name="pref_gpodnet_setlogin_information_title">Zmień informacje logowania</string> + <string name="pref_gpodnet_setlogin_information_sum">Zmień dane logowania konta gpodder.net.</string> + <string name="pref_playback_speed_title">Prędkość odtwarzania</string> + <string name="pref_playback_speed_sum">Dostosuj prędkości dostępne dla odtwarzania audio o zmiennej prędkości</string> + <string name="pref_gpodnet_sethostname_title">Ustaw nazwę hosta</string> + <string name="pref_gpodnet_sethostname_use_default_host">Użyj domyślnego hosta</string> + <!--Search--> + <string name="search_hint">Szukaj kanałów lub odcinków</string> + <string name="found_in_shownotes_label">Znaleziono w notatkach</string> + <string name="found_in_chapters_label">Znaleziono w rozdziałach</string> + <string name="search_status_searching">Wyszukiwanie...</string> + <string name="search_status_no_results">Brak wyników</string> + <string name="search_results_label">Wyniki wyszukiwania</string> + <string name="search_term_label">Szukałeś:\u0020</string> + <string name="search_label">Szukaj</string> + <string name="found_in_title_label">Znaleziono w tytułach</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Pliki OPML pozwalają na przenoszenie podcastów między aplikacjami.</string> + <string name="opml_import_explanation">W celu importu pliku OPML musisz umieścić go w poniższym folderze i nacisnąć przycisk poniżej w celu rozpoczęcia importu.</string> + <string name="start_import_label">Rozpocznij import</string> + <string name="opml_import_label">Import OPML</string> + <string name="opml_directory_error">BŁĄD!</string> + <string name="reading_opml_label">Odczytuję plik OPML</string> + <string name="opml_reader_error">Wystąpił błąd w czasie odczytu dokumentu OPML:</string> + <string name="opml_import_error_dir_empty">Katalog importowania jest pusty.</string> + <string name="select_all_label">Zaznacz wszystko</string> + <string name="deselect_all_label">Odznacz wszystko</string> + <string name="choose_file_to_import_label">Wybierz plik do importu</string> + <string name="opml_export_label">Eksport OPML</string> + <string name="exporting_label">Eksportowanie...</string> + <string name="export_error_label">Błąd eksportu</string> + <string name="opml_export_success_title">Eksport OPML udany.</string> + <string name="opml_export_success_sum">Plik .opml został zapisany do:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Ustaw czas do wyłączenia</string> + <string name="disable_sleeptimer_label">Wyłącz wyłącznik czasowy</string> + <string name="enter_time_here_label">Podaj czas</string> + <string name="sleep_timer_label">Wyłącznik czasowy</string> + <string name="time_left_label">Pozostały czas:\u0020</string> + <string name="time_dialog_invalid_input">Błąd wpisu, czas musi być liczbą całkowitą</string> + <!--Miro Guide--> + <string name="loading_categories_label">Ładuję kategorie...</string> + <string name="browse_miroguide_label">Przeglądaj Miro Guide</string> + <string name="txtv_browse_miroguide_label">Lub przeglądaj Miro Guide:</string> + <string name="miro_guide_label">Miro Guide</string> + <string name="miro_search_hint">Szukaj w Miro Guide</string> + <string name="popular_label">Popularne</string> + <string name="best_rating_label">Najwyższe oceny</string> + <string name="add_feed_label">Dodaj podcast</string> + <string name="miro_feed_added">Kanał jest dodawany</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">KATEGORIE</string> + <string name="gpodnet_toplist_header">TOP PODCASTY</string> + <string name="gpodnet_suggestions_header">SUGESTIE</string> + <string name="gpodnet_search_hint">Szukaj na gpodder.net</string> + <string name="gpodnetauth_login_title">Login</string> + <string name="gpodnetauth_login_descr">Witamy w procesie logowania do gpodder.net. Najpierw podaj swoje dane logowania:</string> + <string name="gpodnetauth_login_butLabel">Login</string> + <string name="gpodnetauth_login_register">Jeśli nie masz jeszcze konta, możesz utworzyć je tutaj:\nhttps://gpodder.net/register/</string> + <string name="username_label">Nazwa użytkownika</string> + <string name="password_label">Hasło</string> + <string name="gpodnetauth_device_title">Wybór urządzenia</string> + <string name="gpodnetauth_device_descr">Utwórz nowe urządzenie dla swojego konta na gpodder.net lub wybierz istniejące:</string> + <string name="gpodnetauth_device_deviceID">Identyfikator urządzenia:\u0020</string> + <string name="gpodnetauth_device_caption">Tytuł</string> + <string name="gpodnetauth_device_butCreateNewDevice">Utwórz nowe urządzenie</string> + <string name="gpodnetauth_device_chooseExistingDevice">Wybierz istniejące urządzenie:</string> + <string name="gpodnetauth_device_errorEmpty">Identyfikator urządzenia nie może być pusty</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Identyfikator urządzenia w użyciu</string> + <string name="gpodnetauth_device_butChoose">Wybierz</string> + <string name="gpodnetauth_finish_title">Logowanie zakończone sukcesem!</string> + <string name="gpodnetauth_finish_descr">Gratulacje! Twoje konto na gpodder.net jest połączone z urządzeniem. AntennaPod będzie automatycznie synchronizować subskrypcje na urządzeniu z kontem na gpodder.net. </string> + <string name="gpodnetauth_finish_butsyncnow">Rozpocznij synchronizację</string> + <string name="gpodnetauth_finish_butgomainscreen">Idź do strony głównej</string> + <string name="gpodnetsync_auth_error_title">Błąd autoryzacji na gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Niepoprawna nazwa użytkownika lub hasło</string> + <string name="gpodnetsync_error_title">Błąd synchronizacji z gpodder.net</string> + <string name="gpodnetsync_error_descr">Wystąpił błąd podczas synchronizacji:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Wybrany folder:</string> + <string name="create_folder_label">Utwórz folder</string> + <string name="choose_data_directory">Wybierz folder danych</string> + <string name="create_folder_msg">Utworzyć nowy folder o nazwie \"%1$s\"?</string> + <string name="create_folder_success">Utworzono nowy folder</string> + <string name="create_folder_error_no_write_access">Nie można zapisać do tego folderu</string> + <string name="create_folder_error_already_exists">Folder już istnieje</string> + <string name="create_folder_error">Nie można utworzyć folderu</string> + <string name="folder_not_empty_dialog_title">Folder nie jest pusty</string> + <string name="folder_not_empty_dialog_msg">Wybrany folder nie jest pusty. Pobierane media i inne pliki będą umieszczane bezpośrednio w folderze, czy kontynuować?</string> + <string name="set_to_default_folder">Wybierz domyślny folder</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Wstrzymaj odtwarzanie zamiast wyciszenia jeśli inna aplikacja chce odtworzyć dźwięk.</string> + <string name="pref_pausePlaybackForFocusLoss_title">Wstrzymaj przy przerwaniu</string> + <!--Online feed view--> + <string name="subscribe_label">Subskrybuj</string> + <string name="subscribed_label">Subskrybowane</string> + <string name="downloading_label">Pobieranie...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Pokaż rozdziały</string> + <string name="rewind_label">Cofnij</string> + <string name="fast_forward_label">Przewiń</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Wideo</string> + <string name="status_playing_label">Odcinek jest odtwarzany</string> + <string name="status_downloading_label">Odcinek jest pobierany</string> + <string name="status_downloaded_label">Odcinek pobrany</string> + <string name="status_unread_label">Nowa pozycja</string> + <string name="in_queue_label">Odcinek jest w kolejce</string> + <string name="new_episodes_count_label">Liczba nowych odcinków</string> + <string name="in_progress_episodes_count_label">Liczba odcinków, których zacząłeś słuchać</string> +</resources> diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 64cfaad00..3c00c27ea 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -96,14 +96,14 @@ <string name="download_error_io_error">Erro de IO</string> <string name="download_error_request_error">Erro de requisição</string> <string name="download_error_db_access">Erro no acesso ao Banco de dados</string> - <string name="downloads_left">\u0020Downloads faltando</string> + <string name="downloads_left">\u0020Downloads restantes</string> <string name="download_notification_title">Baixando dados do podcast</string> <string name="download_report_content">%1$d downloads com sucesso, %2$d falharam</string> <string name="download_log_title_unknown">Título desconhecido</string> <string name="download_type_feed">Feed</string> <string name="download_type_media">Arquivo de mídia</string> <string name="download_type_image">Imagem</string> - <string name="download_request_error_dialog_message_prefix">Ocorreu um erro na tentativa de baixar o arquivo:\u0020</string> + <string name="download_request_error_dialog_message_prefix">Ocorreu um erro durante download do arquivo:\u0020</string> <!--Mediaplayer messages--> <string name="player_error_msg">Erro!</string> <string name="player_stopped_msg">Nenhuma mídia tocando</string> @@ -140,12 +140,11 @@ <string name="action_forbidden_msg">AntennaPod não tem permissão para esta ação. A permissão de acesso do AntennaPod pode ter sido revogada. Você pode re-autenticar ou visitar o website do feed.</string> <string name="access_revoked_title">Acesso revogado</string> <string name="access_revoked_info">Você revogou o token de acesso do AntennaPod com sucesso. Para finalizar o processo, você deve remover esta app da lista de aplicativos aprovados nas configurações de sua conta no website do Flattr.</string> - <string name="flattr_click_success">Registrado no Flattr com sucesso!</string> - <string name="flattring_label">Registrando no Flattr</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">Download Plugin</string> <string name="no_playback_plugin_title">Plugin Não Instalado</string> - <string name="no_playback_plugin_msg">Para ajustar a velocidade de reprodução, uma biblioteca de terceiros deve ser instalada.\n\nToque em \'Download Plugin\' para baixar um plugin grátis na Play Store.\n\nQuaisquer problemas encontrados usando esse plugin não é responsabilidade do AntennaPod e deve ser reportado ao proprietário do plugin.</string> + <string name="no_playback_plugin_msg">Para velocidade variável de reprodução funcionar uma biblioteca de terceiros deve ser instalada.\n\nToque em \'Download Plugin\' para baixar um plugin grátis na Play Store.\n\nQuaisquer problemas encontrados usando esse plugin não é responsabilidade do AntennaPod e deve ser reportado ao proprietário do plugin.</string> <string name="set_playback_speed_label">Velocidades de Reprodução</string> <!--Empty list labels--> <string name="no_items_label">Não existem itens nesta lista.</string> @@ -161,7 +160,7 @@ <string name="playback_pref">Reprodução</string> <string name="network_pref">Rede</string> <string name="pref_autoUpdateIntervall_title">Intervalo de atualização</string> - <string name="pref_autoUpdateIntervall_sum">Especifica o intervalo em que os feeds serão atualizados automaticamente ou desabilita esta funcionalidade</string> + <string name="pref_autoUpdateIntervall_sum">Especifica o intervalo com que os feeds serão atualizados automaticamente ou desabilita esta funcionalidade</string> <string name="pref_downloadMediaOnWifiOnly_sum">Fazer download dos arquivos apenas via rede WiFi</string> <string name="pref_followQueue_title">Reprodução contínua</string> <string name="pref_downloadMediaOnWifiOnly_title">Download de mídia via WiFi</string> @@ -193,13 +192,13 @@ <string name="pref_update_interval_hours_singular">hora</string> <string name="pref_update_interval_hours_manual">Manual</string> <string name="pref_gpodnet_authenticate_title">Login</string> - <string name="pref_gpodnet_authenticate_sum">Faça o login na sua conta gpodder.net para sincronizar suas subscrições.</string> + <string name="pref_gpodnet_authenticate_sum">Faça o login na sua conta gpodder.net para sincronizar suas assinaturas.</string> <string name="pref_gpodnet_logout_title">Sair</string> <string name="pref_gpodnet_logout_toast">Saiu com sucesso</string> <string name="pref_gpodnet_setlogin_information_title">Alterar informações de login</string> <string name="pref_gpodnet_setlogin_information_sum">Alterar informações de login da 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 disponíveis para reprodução de áudio.</string> + <string name="pref_playback_speed_sum">Personalize as velocidades variáveis de reprodução de áudio.</string> <string name="pref_gpodnet_sethostname_title">Configurar hostname</string> <string name="pref_gpodnet_sethostname_use_default_host">Usar host padrão</string> <!--Search--> @@ -213,7 +212,7 @@ <string name="search_label">Pesquisar</string> <string name="found_in_title_label">Encontrado no título</string> <!--OPML import and export--> - <string name="opml_import_txtv_button_lable">Arquivos OPML permitem que você mova seus podcasts de um gestor de podcasts para outro.</string> + <string name="opml_import_txtv_button_lable">Arquivos OPML permitem que você mova seus podcasts de um programa de podcasts para outro.</string> <string name="opml_import_explanation">Para importar um arquivo OPML, você precisa armazená-lo neste diretório e pressionar o botão abaixo para iniciar o processo de importação.</string> <string name="start_import_label">Iniciar importação</string> <string name="opml_import_label">Importação de OPML</string> @@ -231,9 +230,9 @@ <string name="opml_export_success_sum">O arquivo .opml foi gravado em:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Configura desligamento automático</string> - <string name="disable_sleeptimer_label">Desabilita temporizador</string> + <string name="disable_sleeptimer_label">Desabilita desligamento automático</string> <string name="enter_time_here_label">Informe a duração</string> - <string name="sleep_timer_label">Temporizador</string> + <string name="sleep_timer_label">Desligamento automático</string> <string name="time_left_label">Tempo restante:\u0020</string> <string name="time_dialog_invalid_input">Entrada inválida, a duração precisa ser um número inteiro</string> <!--Miro Guide--> @@ -250,6 +249,7 @@ <string name="gpodnet_taglist_header">CATEGORIAS</string> <string name="gpodnet_toplist_header">TOP PODCASTS</string> <string name="gpodnet_suggestions_header">SUGESTÕES</string> + <string name="gpodnet_search_hint">Buscar no gpodder.net</string> <string name="gpodnetauth_login_title">Login</string> <string name="gpodnetauth_login_descr">Bem-vindo ao processo de login gpodder.net. Primeiramente, digite suas informações:</string> <string name="gpodnetauth_login_butLabel">Login</string> @@ -266,12 +266,12 @@ <string name="gpodnetauth_device_errorAlreadyUsed">ID do dispositivo já está em uso</string> <string name="gpodnetauth_device_butChoose">Escolher</string> <string name="gpodnetauth_finish_title">Login realizado com sucesso!</string> - <string name="gpodnetauth_finish_descr">Parabéns! Sua conta gpodder.net agora está ligada ao seu dispositivo. AntennaPod irá, daqui em diante, sincronizar automaticamente subscrições do seu dispositivo com sua conta gpodder.net.</string> + <string name="gpodnetauth_finish_descr">Parabéns! Sua conta gpodder.net agora está conectada ao seu dispositivo. O AntennaPod irá, daqui em diante, sincronizar automaticamente assinaturas do seu dispositivo com sua conta gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Iniciar sincronização agora</string> <string name="gpodnetauth_finish_butgomainscreen">Ir para tela principal</string> - <string name="gpodnetsync_auth_error_title">gpodder.net: Erro de autenticação</string> + <string name="gpodnetsync_auth_error_title">gpodder.net: erro de autenticação</string> <string name="gpodnetsync_auth_error_descr">Nome do usuário ou senha incorreta</string> - <string name="gpodnetsync_error_title">gpodder.net: Erro de sincronização</string> + <string name="gpodnetsync_error_title">gpodder.net: erro de sincronização</string> <string name="gpodnetsync_error_descr">Ocorreu um erro durante a sincronização:\u0020</string> <!--Directory chooser--> <string name="selected_folder_label">Selecionar pasta:</string> @@ -285,10 +285,18 @@ <string name="folder_not_empty_dialog_title">A pasta não está vazia</string> <string name="folder_not_empty_dialog_msg">A pasta que você selecionou não está vazia. Os downloads de mídia e outros arquivos serão colocados diretamente nesta pasta. Deseja mesmo continuar?</string> <string name="set_to_default_folder">Escolher pasta padrão</string> - <string name="pref_pausePlaybackForFocusLoss_sum">Pause a reprodução em vez de baixar o volume quando outro aplicativo reproduzir sons</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pause a reprodução em vez de abaixar o volume quando outro aplicativo reproduzir sons</string> <string name="pref_pausePlaybackForFocusLoss_title">Pausar em interrupções</string> <!--Online feed view--> - <string name="subscribe_label">Subscrever</string> - <string name="subscribed_label">Subscrito</string> + <string name="subscribe_label">Assinar</string> + <string name="subscribed_label">Assinado</string> <string name="downloading_label">Baixando...</string> + <!--Content descriptions for image buttons--> + <string name="show_cover_label">Mostrar imagem</string> + <string name="butAction_label">Mais ações</string> + <string name="status_playing_label">Episódio está sendo reproduzido</string> + <string name="status_downloaded_label">Episódio foi baixado</string> + <string name="status_unread_label">Item é novo</string> + <string name="in_queue_label">Episódio está na fila</string> + <string name="new_episodes_count_label">Numero de novos episódios</string> </resources> diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 392b34dd1..bb9009d18 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -140,12 +140,22 @@ <string name="action_forbidden_msg">O AntennaPod não possui as permissões para esta ação. É possível que o token de acesso ao flattr via AntennaPod tenha sido revogado. Pode efetuar nova autenticação ou aceder ao sítio web do item.</string> <string name="access_revoked_title">Acesso revogado</string> <string name="access_revoked_info">Você revogou o token de acesso do AntennaPod à sua conta. Para concluir o processo, tem que remover esta aplicação da lista de aplicações presentes nas definições de conta no sítio web do flattr.</string> - <string name="flattr_click_success">Flattered com sucesso!</string> - <string name="flattring_label">Flattring</string> + <!--Flattr--> + <string name="flattr_click_success">Flattr de um item!</string> + <string name="flattr_click_success_count">Flattr de %d itens!</string> + <string name="flattr_click_success_queue">Flattr: %s</string> + <string name="flattr_click_failure_count">Falha ao efetuar flattr de %d itens!</string> + <string name="flattr_click_failure">Não flattr: %s.</string> + <string name="flattr_click_enqueued">O flattr deste item será feito mais tarde</string> + <string name="flattring_thing">Flattring %s</string> + <string name="flattring_label">O AntennaPod está a flattring</string> + <string name="flattrd_label">O AntennaPod fez o flattr</string> + <string name="flattrd_failed_label">O AntennaPod não fez o flattr</string> + <string name="flattr_retrieving_status">A obter itens com flattr</string> <!--Variable Speed--> <string name="download_plugin_label">Transferir extra</string> <string name="no_playback_plugin_title">Extra não instalado</string> - <string name="no_playback_plugin_msg">Para melhorar a reprodução, deve transferir e instalar um biblioteca de terceiros.\nClique Transferir extra para transferir o extra através da loja Google..\n\nSe encontrar problemas ao utilizar esta biblioteca, os programadores do AntennaPod não podem ser responsabilizados e deve contactar o programador do extra.</string> + <string name="no_playback_plugin_msg">Para melhorar a reprodução, deve transferir e instalar um biblioteca de terceiros.\nClique Transferir extra para transferir o extra através da loja Google.\n\nSe encontrar problemas ao utilizar esta biblioteca, os programadores do AntennaPod não podem ser responsabilizados e deve contactar o programador do extra.</string> <string name="set_playback_speed_label">Velocidades de reprodução</string> <!--Empty list labels--> <string name="no_items_label">Não existem itens na lista.</string> @@ -176,10 +186,12 @@ <string name="pref_flattr_this_app_sum">Ajude no desenvolvimento do AntennaPod através do Flattr. Obrigado!</string> <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">Flattr de episódios com 80 porcento de reprodução.</string> <string name="pref_display_only_episodes_title">Mostrar apenas episódios</string> <string name="pref_display_only_episodes_sum">Apenas mostrar itens que possuam episódios.</string> <string name="user_interface_label">Interface</string> - <string name="pref_set_theme_title">Escolha o tema</string> + <string name="pref_set_theme_title">Tema</string> <string name="pref_set_theme_sum">Mudar o aspeto do AntennaPod.</string> <string name="pref_automatic_download_title">Transferência automática</string> <string name="pref_automatic_download_sum">Configure a transferência automática dos episódios.</string> @@ -292,4 +304,21 @@ <string name="subscribe_label">Subscrever</string> <string name="subscribed_label">Subscrito</string> <string name="downloading_label">Transferência...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Mostrar capítulos</string> + <string name="show_shownotes_label">Mostrar notas</string> + <string name="show_cover_label">Mostrar imagem</string> + <string name="rewind_label">Recuar</string> + <string name="fast_forward_label">Avanço rápido</string> + <string name="media_type_audio_label">Áudio</string> + <string name="media_type_video_label">Vídeo</string> + <string name="navigate_upwards_label">Navegar para cima</string> + <string name="butAction_label">Mais ações</string> + <string name="status_playing_label">Episódio em reprodução</string> + <string name="status_downloading_label">Episódio a ser transferido</string> + <string name="status_downloaded_label">Episódio transferido</string> + <string name="status_unread_label">Novo item</string> + <string name="in_queue_label">Episódio está na fila</string> + <string name="new_episodes_count_label">Número de novos episódios</string> + <string name="in_progress_episodes_count_label">Número de episódios que já foi iniciada a reprodução</string> </resources> diff --git a/res/values-ro-rRO/strings.xml b/res/values-ro-rRO/strings.xml index b2a3d6526..d610a27f0 100644 --- a/res/values-ro-rRO/strings.xml +++ b/res/values-ro-rRO/strings.xml @@ -27,7 +27,6 @@ <string name="author_label">Autor</string> <string name="language_label">Limbă</string> <string name="podcast_settings_label">Setări</string> - <string name="cover_label">Copertă</string> <string name="error_label">Eroare</string> <string name="error_msg_prefix">A avut loc o eroare:</string> <string name="refresh_label">Reîncarcă</string> @@ -132,8 +131,7 @@ <string name="action_forbidden_msg">AntennaPod nu are permisiuni pentru această acțiune. Motivul poate fi că tokenul de acces al AntennaPod pentru contul vostru a fost revocat. Vă puteți fie re-autentifica fie vizita direct site-ul.</string> <string name="access_revoked_title">Acces revocat</string> <string name="access_revoked_info">Ați revocat cu succes accesul AntennaPod la contul vostru. Pentru a completa acest proces trebuie să ștergeți aplicația din lista de aplicații aprobate din setările contului de pe site-ul flattr.</string> - <string name="flattr_click_success">Ați flattr cu succes!</string> - <string name="flattring_label">Flattring</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">Descarcă plugin</string> <string name="no_playback_plugin_title">Plugin neinstalat</string> @@ -263,4 +261,5 @@ <string name="subscribe_label">Abonează-te</string> <string name="subscribed_label">Abonat</string> <string name="downloading_label">Se descarcă...</string> + <!--Content descriptions for image buttons--> </resources> diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 0d40685d7..8b69c340c 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -28,16 +28,16 @@ <string name="author_label">Автор</string> <string name="language_label">Язык</string> <string name="podcast_settings_label">Настройки</string> - <string name="cover_label">Обложка</string> + <string name="cover_label">Изображение</string> <string name="error_label">Ошибка</string> <string name="error_msg_prefix">Произошла ошибка:</string> <string name="refresh_label">Обновить</string> <string name="external_storage_error_msg">Внешний носитель недоступен. Убедитесь что внешний носитель установлен, иначе приложение не сможет нормально работать.</string> <string name="chapters_label">Разделы</string> - <string name="shownotes_label">Описание</string> + <string name="shownotes_label">Заметки к эпизоду</string> <string name="description_label">Описание</string> <string name="most_recent_prefix">Следующий эпизод:\u0020</string> - <string name="episodes_suffix">\u0020 выпуск(ов)</string> + <string name="episodes_suffix">\u0020выпуск(ов)</string> <string name="published_prefix">Опубликовано:\u0020</string> <string name="length_prefix">Продолжительность:\u0020</string> <string name="size_prefix">Размер:\u0020</string> @@ -56,8 +56,8 @@ <string name="mark_all_read_label">Отметить все как прочитанное</string> <string name="show_info_label">Показать информацию</string> <string name="remove_feed_label">Удалить канал</string> - <string name="share_link_label">Поделиться ссылкой на сайт</string> - <string name="share_source_label">Поделиться ссылкой на канал</string> + <string name="share_link_label">Ссылка на сайт</string> + <string name="share_source_label">Ссылка на канал</string> <string name="feed_delete_confirmation_msg">Подтвердите удаление канала и ВСЕХ загруженных с этого канала выпусков.</string> <string name="feed_remover_msg">Удаление канала</string> <!--actions on feeditems--> @@ -81,7 +81,7 @@ <string name="download_pending">Загрузка в ожидании</string> <string name="download_running">Загрузка в процессе</string> <string name="download_error_device_not_found">Устройство хранения не найдено</string> - <string name="download_error_insufficient_space">Недостаточно памяти</string> + <string name="download_error_insufficient_space">Недостаточно места</string> <string name="download_error_file_error">Ошибка файла</string> <string name="download_error_http_data_error">Ошибка протокола HTTP</string> <string name="download_error_error_unknown">Неизвестная ошибка</string> @@ -140,8 +140,8 @@ <string name="action_forbidden_msg">AntennaPod не имеет прав для выполнения этого действия. Возможно, доступ к вашему аккаунту был отозван. Вы можете авторизоваться заново или посетить сайт, которому вы пожертвовали через Flattr.</string> <string name="access_revoked_title">Доступ отозван</string> <string name="access_revoked_info">Вы успешно отключили AntennaPod от вашего аккаунта в Flattr. Чтобы завершить этот процесс вам нужно удалить AntennaPod из списка приложений подключенных к вашему аккаунту на сайте Flattr.</string> - <string name="flattr_click_success">Поддержка через Flattr прошла успешно!</string> - <string name="flattring_label">Отправляется запрос на Flattr</string> + <!--Flattr--> + <string name="flattr_click_success_queue">Поддержано через Flattr: %s.</string> <!--Variable Speed--> <string name="download_plugin_label">Загрузить плагин</string> <string name="no_playback_plugin_title">Плагин не установлен</string> @@ -176,6 +176,8 @@ <string name="pref_flattr_this_app_sum">Поддержите разработку AntennaPod через Flattr. Спасибо!</string> <string name="pref_revokeAccess_title">Отозвать доступ</string> <string name="pref_revokeAccess_sum">Отменить доступ этого приложения к вашему аккаунту Flattr.</string> + <string name="pref_auto_flattr_title">Автоматически поддерживать через Flattr</string> + <string name="pref_auto_flattr_sum">Поддерживать через Flattr эпизоды, прослушанные на 80%</string> <string name="pref_display_only_episodes_title">Показывать только выпуски</string> <string name="pref_display_only_episodes_sum">Показывать только те элементы списка, которые содержат выпуски</string> <string name="user_interface_label">Интерфейс</string> @@ -189,8 +191,8 @@ <string name="pref_theme_title_light">Светлая</string> <string name="pref_theme_title_dark">Тёмная</string> <string name="pref_episode_cache_unlimited">Неограничен</string> - <string name="pref_update_interval_hours_plural">Часы</string> - <string name="pref_update_interval_hours_singular">Час</string> + <string name="pref_update_interval_hours_plural">ч.</string> + <string name="pref_update_interval_hours_singular">час</string> <string name="pref_update_interval_hours_manual">Вручную</string> <string name="pref_gpodnet_authenticate_title">Вход в gpodder.net</string> <string name="pref_gpodnet_authenticate_sum">Вход в ваш аккаунт gpodder.net для синхронизации ваших подписок.</string> @@ -258,7 +260,7 @@ <string name="username_label">Имя пользователя</string> <string name="password_label">Пароль</string> <string name="gpodnetauth_device_title">Выбор устройства</string> - <string name="gpodnetauth_device_descr">Создайте новое устройство, чтобы использовать ваш аккаунта на gpodder.net или выберите существующее:</string> + <string name="gpodnetauth_device_descr">Создайте новое устройство, чтобы использовать ваш аккаунт на gpodder.net или выберите существующее:</string> <string name="gpodnetauth_device_deviceID">Device ID:\u0020</string> <string name="gpodnetauth_device_caption">Название устройства</string> <string name="gpodnetauth_device_butCreateNewDevice">Создайте новое устройство</string> @@ -268,12 +270,12 @@ <string name="gpodnetauth_device_butChoose">Выберите</string> <string name="gpodnetauth_finish_title">Авторизация успешна!</string> <string name="gpodnetauth_finish_descr">Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.net</string> - <string name="gpodnetauth_finish_butsyncnow">Начать синхронизацию сейчас</string> + <string name="gpodnetauth_finish_butsyncnow">Начать синхронизацию</string> <string name="gpodnetauth_finish_butgomainscreen">Перейти на главный экран</string> <string name="gpodnetsync_auth_error_title">Ошибка авторизации на gpodder.net</string> <string name="gpodnetsync_auth_error_descr">Неправильное имя пользователя или пароль</string> <string name="gpodnetsync_error_title">Ошибка синхронизации с gpodder.net</string> - <string name="gpodnetsync_error_descr">Произошла ошибка во время синзронизации:\u0020</string> + <string name="gpodnetsync_error_descr">Произошла ошибка во время синхронизации:\u0020</string> <!--Directory chooser--> <string name="selected_folder_label">Выбранная папка:</string> <string name="create_folder_label">Создать папку</string> @@ -292,4 +294,21 @@ <string name="subscribe_label">Подписаться</string> <string name="subscribed_label">Подписка оформлена</string> <string name="downloading_label">Загрузка...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Показать разделы</string> + <string name="show_shownotes_label">Показать заметки к эпизодам</string> + <string name="show_cover_label">Показать изображение</string> + <string name="rewind_label">Назад</string> + <string name="fast_forward_label">Вперед</string> + <string name="media_type_audio_label">Аудио</string> + <string name="media_type_video_label">Видео</string> + <string name="navigate_upwards_label">Перейти выше</string> + <string name="butAction_label">Другие действия</string> + <string name="status_playing_label">Эпизод воспроизводится</string> + <string name="status_downloading_label">Эпизод загружается</string> + <string name="status_downloaded_label">Эпизод загружен</string> + <string name="status_unread_label">Новый</string> + <string name="in_queue_label">Эпизод в очереди</string> + <string name="new_episodes_count_label">Количество новых эпизодов</string> + <string name="in_progress_episodes_count_label">Количество начатых эпизодов</string> </resources> diff --git a/res/values-sv-rSE/strings.xml b/res/values-sv-rSE/strings.xml index ccfcddac4..426dd7b37 100644 --- a/res/values-sv-rSE/strings.xml +++ b/res/values-sv-rSE/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">Skapare</string> <string name="language_label">Språk</string> <string name="podcast_settings_label">Inställningar</string> - <string name="cover_label">Omslag</string> + <string name="cover_label">Bild</string> <string name="error_label">Fel</string> <string name="error_msg_prefix">Ett fel inträffade:</string> <string name="refresh_label">Uppdatera</string> @@ -118,7 +118,7 @@ <string name="playbackservice_notification_title">Spelar podcast</string> <string name="playbackservice_notification_content">Tryck här för mer information</string> <!--Navigation--> - <string name="show_download_log">Visa log</string> + <string name="show_download_log">Visa logg</string> <string name="show_player_label">Visa spelare</string> <!--Queue operations--> <string name="clear_queue_label">Rensa kön</string> @@ -140,8 +140,18 @@ <string name="action_forbidden_msg">AntennaPod saknar behörighet för den här åtgärden. Anledningen till detta kan vara att AntennaPods tillgång till ditt konto har återkallats. Du kan antingen åter autentisera AntennaPod eller besöka hemsidan istället.</string> <string name="access_revoked_title">Tillgång återkallad</string> <string name="access_revoked_info">Du har nu återkallat AntennaPods tillgång till ditt konto. För att slutföra processen, måste du ta bort den här appen från listan godkända appar i dina kontoinställningar på Flattrs hemsida.</string> - <string name="flattr_click_success">Framgångsrikt flattrat denna sak!</string> - <string name="flattring_label">Flattrar</string> + <!--Flattr--> + <string name="flattr_click_success">Flattrade en sak!</string> + <string name="flattr_click_success_count">Flattrade %d saker!</string> + <string name="flattr_click_success_queue">Flattrade: %s.</string> + <string name="flattr_click_failure_count">Misslyckades att flattra %d saker!</string> + <string name="flattr_click_failure">Ej flattrade: %s.</string> + <string name="flattr_click_enqueued">Saker som kommer att flattras senare</string> + <string name="flattring_thing">Flattrar %s</string> + <string name="flattring_label">AnntennaPod flattrar</string> + <string name="flattrd_label">AntennaPod har flattrat</string> + <string name="flattrd_failed_label">AntennaPod misslyckades att flattra</string> + <string name="flattr_retrieving_status">Hämtar flattrade saker</string> <!--Variable Speed--> <string name="download_plugin_label">Ladda ner tillägg</string> <string name="no_playback_plugin_title">Tillägg ej installerat</string> @@ -176,6 +186,8 @@ <string name="pref_flattr_this_app_sum">Stöd utvecklingen av AntennaPod genom att flattra den. Tack!</string> <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">Flattra episoder som har spelats minst 80%.</string> <string name="pref_display_only_episodes_title">Visa endast episoder</string> <string name="pref_display_only_episodes_sum">Visa endast objekt som har minst ett avsnitt.</string> <string name="user_interface_label">Användargränssnitt</string> @@ -292,4 +304,21 @@ <string name="subscribe_label">Prenumerera</string> <string name="subscribed_label">Prenumererar</string> <string name="downloading_label">Laddar ner...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Visa kapitel</string> + <string name="show_shownotes_label">Visa shownotes</string> + <string name="show_cover_label">Visa bild</string> + <string name="rewind_label">Backa</string> + <string name="fast_forward_label">Snabbspola</string> + <string name="media_type_audio_label">Ljud</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Navigera upp</string> + <string name="butAction_label">Fler åtgärder</string> + <string name="status_playing_label">Episoden spelas</string> + <string name="status_downloading_label">Episoden laddas ner</string> + <string name="status_downloaded_label">Episoden är nedladdad</string> + <string name="status_unread_label">Föremålet är nytt</string> + <string name="in_queue_label">Episoden är i kön</string> + <string name="new_episodes_count_label">Antal nya episoder</string> + <string name="in_progress_episodes_count_label">Antal episoder du har börjat lyssna på</string> </resources> diff --git a/res/values-uk-rUA/strings.xml b/res/values-uk-rUA/strings.xml index 32d6e5101..534c46429 100644 --- a/res/values-uk-rUA/strings.xml +++ b/res/values-uk-rUA/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">Автор</string> <string name="language_label">Мова</string> <string name="podcast_settings_label">Налаштування</string> - <string name="cover_label">Обкладинка</string> + <string name="cover_label">Зображення</string> <string name="error_label">Помилка</string> <string name="error_msg_prefix">Трапилась помілка:</string> <string name="refresh_label">Оновити</string> @@ -140,8 +140,18 @@ <string name="action_forbidden_msg">AntennaPod не маэ дозвілу це зробити. Можливо відкликаний доступ до AntennaPod. Або ввідіть логін пароль в налаштуваннях або зробить це на сайті</string> <string name="access_revoked_title">Доступ відкликано</string> <string name="access_revoked_info">Ви відкликали доступ AntennaPod до облікового запису. Для закінчення процессу вам потрібно видалити додаток з затвержденного списку в вашому облікову запису на сайті flattr</string> - <string name="flattr_click_success">Успішно flattr це</string> - <string name="flattring_label">Йде flattr</string> + <!--Flattr--> + <string name="flattr_click_success">Flattr\'ed one thing!</string> + <string name="flattr_click_success_count">Flattr\'ed %d things!</string> + <string name="flattr_click_success_queue">Flattr\'ed: %s.</string> + <string name="flattr_click_failure_count">Failed to flattr %d things!</string> + <string name="flattr_click_failure">Not flattr\'ed: %s.</string> + <string name="flattr_click_enqueued">Thing will be flattr\'ed later</string> + <string name="flattring_thing">Flattring %s</string> + <string name="flattring_label">AntennaPod is flattring</string> + <string name="flattrd_label">AntennaPod has flattr\'ed</string> + <string name="flattrd_failed_label">AntennaPod flattr failed</string> + <string name="flattr_retrieving_status">Retrieving flattr\'ed things</string> <!--Variable Speed--> <string name="download_plugin_label">Завантажити Plugin</string> <string name="no_playback_plugin_title">Plugin не встановлено</string> @@ -176,6 +186,8 @@ <string name="pref_flattr_this_app_sum">Підтримайте розробку AntennaPod за допомогою flattr. Дякую!</string> <string name="pref_revokeAccess_title">Відкликати доступ</string> <string name="pref_revokeAccess_sum">Відкликати дозвіл на доступ до вашого flattr з цього додатку</string> + <string name="pref_auto_flattr_title">Automatic Flattr</string> + <string name="pref_auto_flattr_sum">Flattr episodes of which 80% have been played.</string> <string name="pref_display_only_episodes_title">Показувати тільки епізоди</string> <string name="pref_display_only_episodes_sum">Відображати тільки канали з наявними епізодами</string> <string name="user_interface_label">Зовнішній вид</string> @@ -248,7 +260,7 @@ <string name="miro_feed_added">Канал додано</string> <!--gpodder.net--> <string name="gpodnet_taglist_header">КАТЕГОРІЇ</string> - <string name="gpodnet_toplist_header">ТОП ПОКАДСТІВ</string> + <string name="gpodnet_toplist_header">ТОП ПОДКАСТІВ</string> <string name="gpodnet_suggestions_header">РЕКОМЕНДАЦІЇ</string> <string name="gpodnet_search_hint">Пошук на gpodder.net</string> <string name="gpodnetauth_login_title">Логін</string> @@ -292,4 +304,21 @@ <string name="subscribe_label">Підписатися</string> <string name="subscribed_label">Підписано</string> <string name="downloading_label">Завантаження...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Показати глави</string> + <string name="show_shownotes_label">Показати нотатки</string> + <string name="show_cover_label">Показати зображення</string> + <string name="rewind_label">Перемотка назад</string> + <string name="fast_forward_label">Перемотка вперед</string> + <string name="media_type_audio_label">Звук</string> + <string name="media_type_video_label">Відео</string> + <string name="navigate_upwards_label">Догори</string> + <string name="butAction_label">Додаткові дії</string> + <string name="status_playing_label">Епізод програється</string> + <string name="status_downloading_label">Епізод завантажується</string> + <string name="status_downloaded_label">Епізод завантажено</string> + <string name="status_unread_label">Нове</string> + <string name="in_queue_label">Епізод чекає в черзі</string> + <string name="new_episodes_count_label">Кількість нових епізодів</string> + <string name="in_progress_episodes_count_label">Кількість епізодів що ви почали слухати</string> </resources> diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 6cc612374..4bb3400c4 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -28,7 +28,7 @@ <string name="author_label">作者</string> <string name="language_label">语言</string> <string name="podcast_settings_label">设置</string> - <string name="cover_label">封面</string> + <string name="cover_label">图片</string> <string name="error_label">错误</string> <string name="error_msg_prefix">出错:</string> <string name="refresh_label">刷新</string> @@ -140,8 +140,7 @@ <string name="action_forbidden_msg">AntennaPod 没有权限执行本动作. 原因可能是: AntennaPod 对您账户的访问令牌被撤销. 你可以重新\"验证\"或访问该网站来授权.</string> <string name="access_revoked_title">撤销访问</string> <string name="access_revoked_info">您已经成功撤销 AntennaPod 对账户令牌的访问. 为了完成这个过程, 您必须到 Flattr 网站 \"账户设置->已批准应用\" 列表内删除本应用.</string> - <string name="flattr_click_success">Flattr 成功!</string> - <string name="flattring_label">Flattring</string> + <!--Flattr--> <!--Variable Speed--> <string name="download_plugin_label">插件下载</string> <string name="no_playback_plugin_title">插件没有安装</string> @@ -153,7 +152,7 @@ <!--Preferences--> <string name="other_pref">其他</string> <string name="about_pref">关于</string> - <string name="queue_label">清空播放</string> + <string name="queue_label">播放列表</string> <string name="services_label">服务</string> <string name="flattr_label">Flattr</string> <string name="pref_pauseOnHeadsetDisconnect_sum">耳机断开时暂停播放 </string> @@ -292,4 +291,21 @@ <string name="subscribe_label">订阅</string> <string name="subscribed_label">已订阅</string> <string name="downloading_label">下载中...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">显示章节</string> + <string name="show_shownotes_label">显示笔记</string> + <string name="show_cover_label">显示图片</string> + <string name="rewind_label">回放</string> + <string name="fast_forward_label">快进</string> + <string name="media_type_audio_label">音频</string> + <string name="media_type_video_label">视频</string> + <string name="navigate_upwards_label">向上导航</string> + <string name="butAction_label">更多动作</string> + <string name="status_playing_label">曲目正在播放</string> + <string name="status_downloading_label">曲目正在下载</string> + <string name="status_downloaded_label">曲目已下载</string> + <string name="status_unread_label">新项目</string> + <string name="in_queue_label">曲目已经在播放列表中</string> + <string name="new_episodes_count_label">新曲目数</string> + <string name="in_progress_episodes_count_label">已收听曲目数</string> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 8c05e42b5..43bedadee 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -35,7 +35,7 @@ <string name="author_label">Author</string> <string name="language_label">Language</string> <string name="podcast_settings_label">Settings</string> - <string name="cover_label">Cover</string> + <string name="cover_label">Picture</string> <string name="error_label">Error</string> <string name="error_msg_prefix">An error occurred:</string> <string name="refresh_label">Refresh</string> @@ -154,9 +154,20 @@ <string name="action_forbidden_msg">AntennaPod has no permission for this action. The reason for this could be that the access token of AntennaPod to your account has been revoked. You can either re-reauthenticate or visit the website of the thing instead.</string> <string name="access_revoked_title">Access revoked</string> <string name="access_revoked_info">You have successfully revoked AntennaPod\'s access token to your account. In order to complete the process, you have to remove this app from the list of approved applications in your account settings on the flattr website.</string> - <string name="flattr_click_success">Successfully flattred this thing!</string> - <string name="flattring_label">Flattring</string> + <!-- Flattr --> + <string name="flattr_click_success">Flattr\'ed one thing!</string> + <string name="flattr_click_success_count">Flattr\'ed %d things!</string> + <string name="flattr_click_success_queue">Flattr\'ed: %s.</string> + <string name="flattr_click_failure_count">Failed to flattr %d things!</string> + <string name="flattr_click_failure">Not flattr\'ed: %s.</string> + <string name="flattr_click_enqueued">Thing will be flattr\'ed later</string> + <string name="flattring_thing">Flattring %s</string> + <string name="flattring_label">AntennaPod is flattring</string> + <string name="flattrd_label">AntennaPod has flattr\'ed</string> + <string name="flattrd_failed_label">AntennaPod flattr failed</string> + <string name="flattr_retrieving_status">Retrieving flattr\'ed things</string> + <!-- Variable Speed --> <string name="download_plugin_label">Download Plugin</string> <string name="no_playback_plugin_title">Plugin Not Installed</string> @@ -193,6 +204,8 @@ <string name="pref_flattr_this_app_sum">Support the development of AntennaPod by flattring it. Thanks!</string> <string name="pref_revokeAccess_title">Revoke access</string> <string name="pref_revokeAccess_sum">Revoke the access permission to your flattr account for this app.</string> + <string name="pref_auto_flattr_title">Automatic Flattr</string> + <string name="pref_auto_flattr_sum">Flattr episodes of which 80% have been played.</string> <string name="pref_display_only_episodes_title">Display only episodes</string> <string name="pref_display_only_episodes_sum">Display only items which also have an episode.</string> <string name="user_interface_label">User Interface</string> @@ -319,4 +332,22 @@ <string name="subscribe_label">Subscribe</string> <string name="subscribed_label">Subscribed</string> <string name="downloading_label">Downloading...</string> + + <!-- Content descriptions for image buttons --> + <string name="show_chapters_label">Show chapters</string> + <string name="show_shownotes_label">Show shownotes</string> + <string name="show_cover_label">Show picture</string> + <string name="rewind_label">Rewind</string> + <string name="fast_forward_label">Fast forward</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Navigate upwards</string> + <string name="butAction_label">More actions</string> + <string name="status_playing_label">Episode is being played</string> + <string name="status_downloading_label">Episode is being downloaded</string> + <string name="status_downloaded_label">Episode is downloaded</string> + <string name="status_unread_label">Item is new</string> + <string name="in_queue_label">Episode is in the queue</string> + <string name="new_episodes_count_label">Number of new episodes</string> + <string name="in_progress_episodes_count_label">Number of episodes you have started listening to</string> </resources> diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index cba297570..8e0b886de 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -83,6 +83,12 @@ <intent android:action=".activities.FlattrAuthActivity"/> </PreferenceScreen> + <CheckBoxPreference + android:defaultValue="false" + android:enabled="false" + android:key="pref_auto_flattr" + android:summary="@string/pref_auto_flattr_sum" + android:title="@string/pref_auto_flattr_title" /> <Preference android:key="prefRevokeAccess" android:summary="@string/pref_revokeAccess_sum" @@ -109,7 +115,6 @@ android:key="pref_gpodnet_hostname" android:title="@string/pref_gpodnet_sethostname_title"/> </PreferenceScreen> - </PreferenceCategory> <PreferenceCategory android:title="@string/other_pref"> <Preference diff --git a/src/de/danoeh/antennapod/AppConfig.java b/src/de/danoeh/antennapod/AppConfig.java index e79eb64e8..f95fca7fc 100644 --- a/src/de/danoeh/antennapod/AppConfig.java +++ b/src/de/danoeh/antennapod/AppConfig.java @@ -4,5 +4,5 @@ public final class AppConfig { /** Should be used for debug logging. */ public final static boolean DEBUG = true; /** Should be used when setting User-Agent header for HTTP-requests. */ - public final static String USER_AGENT = "AntennaPod/0.9.8.0"; + public final static String USER_AGENT = "AntennaPod/0.9.8.1"; } diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java index db4373036..a55f8120e 100644 --- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -11,15 +11,10 @@ import android.support.v4.app.ListFragment; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; -import android.view.Window; import android.view.View.OnLongClickListener; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.ImageButton; +import android.view.Window; +import android.widget.*; import android.widget.ImageView.ScaleType; -import android.widget.ListView; -import android.widget.TextView; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChapterListAdapter; @@ -31,501 +26,525 @@ import de.danoeh.antennapod.feed.SimpleChapter; import de.danoeh.antennapod.fragment.CoverFragment; import de.danoeh.antennapod.fragment.ItemDescriptionFragment; import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.util.playback.ExternalMedia; import de.danoeh.antennapod.util.playback.Playable; -/** Activity for playing audio files. */ +/** + * Activity for playing audio files. + */ public class AudioplayerActivity extends MediaplayerActivity { - private static final int POS_COVER = 0; - private static final int POS_DESCR = 1; - private static final int POS_CHAPTERS = 2; - private static final int NUM_CONTENT_FRAGMENTS = 3; - - final String TAG = "AudioplayerActivity"; - private static final String PREFS = "AudioPlayerActivityPreferences"; - private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition"; - private static final String PREF_PLAYABLE_ID = "playableId"; - - private Fragment[] detachedFragments; - - private CoverFragment coverFragment; - private ItemDescriptionFragment descriptionFragment; - private ListFragment chapterFragment; - - private Fragment currentlyShownFragment; - private int currentlyShownPosition = -1; - /** Used if onResume was called without loadMediaInfo. */ - private int savedPosition = -1; - - private TextView txtvTitle; - private TextView txtvFeed; - private Button butPlaybackSpeed; - private ImageButton butNavLeft; - private ImageButton butNavRight; - - private void resetFragmentView() { - FragmentTransaction fT = getSupportFragmentManager().beginTransaction(); - - if (coverFragment != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Removing cover fragment"); - fT.remove(coverFragment); - } - if (descriptionFragment != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Removing description fragment"); - fT.remove(descriptionFragment); - } - if (chapterFragment != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Removing chapter fragment"); - fT.remove(chapterFragment); - } - if (currentlyShownFragment != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Removing currently shown fragment"); - fT.remove(currentlyShownFragment); - } - for (int i = 0; i < detachedFragments.length; i++) { - Fragment f = detachedFragments[i]; - if (f != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Removing detached fragment"); - fT.remove(f); - } - } - fT.commit(); - currentlyShownFragment = null; - coverFragment = null; - descriptionFragment = null; - chapterFragment = null; - currentlyShownPosition = -1; - detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS]; - } - - @Override - protected void onStop() { - super.onStop(); - if (AppConfig.DEBUG) - Log.d(TAG, "onStop"); - - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayShowTitleEnabled(false); - detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS]; - } - - private void savePreferences() { - if (AppConfig.DEBUG) - Log.d(TAG, "Saving preferences"); - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - if (currentlyShownPosition >= 0 && controller != null - && controller.getMedia() != null) { - editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, - currentlyShownPosition); - editor.putString(PREF_PLAYABLE_ID, controller.getMedia() - .getIdentifier().toString()); - } else { - editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1); - editor.putString(PREF_PLAYABLE_ID, ""); - } - editor.commit(); - - savedPosition = currentlyShownPosition; - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - // super.onSaveInstanceState(outState); would cause crash - if (AppConfig.DEBUG) - Log.d(TAG, "onSaveInstanceState"); - } - - @Override - protected void onPause() { - savePreferences(); - resetFragmentView(); - super.onPause(); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - restoreFromPreferences(); - } - - /** - * Tries to restore the selected fragment position from the Activity's - * preferences. - * - * @return true if restoreFromPrefernces changed the activity's state - * */ - private boolean restoreFromPreferences() { - if (AppConfig.DEBUG) - Log.d(TAG, "Restoring instance state"); - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, - -1); - String playableId = prefs.getString(PREF_PLAYABLE_ID, ""); - - if (savedPosition != -1 - && controller != null - && controller.getMedia() != null - && controller.getMedia().getIdentifier().toString() - .equals(playableId)) { - switchToFragment(savedPosition); - return true; - } else if (controller == null || controller.getMedia() == null) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Couldn't restore from preferences: controller or media was null"); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: " - + savedPosition + ", id: " + playableId); - - } - return false; - } - - @Override - protected void onResume() { - super.onResume(); - if (getIntent().getAction() != null - && getIntent().getAction().equals(Intent.ACTION_VIEW)) { - Intent intent = getIntent(); - if (AppConfig.DEBUG) - Log.d(TAG, "Received VIEW intent: " - + intent.getData().getPath()); - ExternalMedia media = new ExternalMedia(intent.getData().getPath(), - MediaType.AUDIO); - Intent launchIntent = new Intent(this, PlaybackService.class); - launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, - true); - launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); - launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, - true); - startService(launchIntent); - } - if (savedPosition != -1) { - switchToFragment(savedPosition); - } - - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } - - @Override - protected void onAwaitingVideoSurface() { - startActivity(new Intent(this, VideoplayerActivity.class)); - } - - @Override - protected void postStatusMsg(int resId) { - setSupportProgressBarIndeterminateVisibility(resId == R.string.player_preparing_msg - || resId == R.string.player_seeking_msg - || resId == R.string.player_buffering_msg); - } - - @Override - protected void clearStatusMsg() { - setSupportProgressBarIndeterminateVisibility(false); - } - - /** - * Changes the currently displayed fragment. - * - * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS - * */ - private void switchToFragment(int pos) { - if (AppConfig.DEBUG) - Log.d(TAG, "Switching contentView to position " + pos); - if (currentlyShownPosition != pos && controller != null) { - Playable media = controller.getMedia(); - if (media != null) { - FragmentTransaction ft = getSupportFragmentManager() - .beginTransaction(); - if (currentlyShownFragment != null) { - detachedFragments[currentlyShownPosition] = currentlyShownFragment; - ft.detach(currentlyShownFragment); - } - switch (pos) { - case POS_COVER: - if (coverFragment == null) { - Log.i(TAG, "Using new coverfragment"); - coverFragment = CoverFragment.newInstance(media); - } - currentlyShownFragment = coverFragment; - break; - case POS_DESCR: - if (descriptionFragment == null) { - descriptionFragment = ItemDescriptionFragment - .newInstance(media, true); - } - currentlyShownFragment = descriptionFragment; - break; - case POS_CHAPTERS: - if (chapterFragment == null) { - chapterFragment = new ListFragment() { - - @Override - public void onListItemClick(ListView l, View v, - int position, long id) { - super.onListItemClick(l, v, position, id); - Chapter chapter = (Chapter) this - .getListAdapter().getItem(position); - controller.seekToChapter(chapter); - } - - }; - chapterFragment.setListAdapter(new ChapterListAdapter( - AudioplayerActivity.this, 0, media - .getChapters(), media)); - } - currentlyShownFragment = chapterFragment; - break; - } - if (currentlyShownFragment != null) { - currentlyShownPosition = pos; - if (detachedFragments[pos] != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Reattaching fragment at position " - + pos); - ft.attach(detachedFragments[pos]); - } else { - ft.add(R.id.contentView, currentlyShownFragment); - } - ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); - ft.disallowAddToBackStack(); - ft.commit(); - updateNavButtonDrawable(); - } - } - } - } - - private void updateNavButtonDrawable() { - TypedArray drawables = obtainStyledAttributes(new int[] { - R.attr.navigation_shownotes, R.attr.navigation_chapters }); - final Playable media = controller.getMedia(); - if (butNavLeft != null && butNavRight != null && media != null) { - switch (currentlyShownPosition) { - case POS_COVER: - butNavLeft.setScaleType(ScaleType.CENTER); - butNavLeft.setImageDrawable(drawables.getDrawable(0)); - butNavRight.setImageDrawable(drawables.getDrawable(1)); - break; - case POS_DESCR: - butNavLeft.setScaleType(ScaleType.CENTER_CROP); - butNavLeft.post(new Runnable() { - - @Override - public void run() { - ImageLoader.getInstance().loadThumbnailBitmap(media, - butNavLeft); - } - }); - butNavRight.setImageDrawable(drawables.getDrawable(1)); - break; - case POS_CHAPTERS: - butNavLeft.setScaleType(ScaleType.CENTER_CROP); - butNavLeft.post(new Runnable() { - - @Override - public void run() { - ImageLoader.getInstance().loadThumbnailBitmap(media, - butNavLeft); - } - }); - butNavRight.setImageDrawable(drawables.getDrawable(0)); - break; - } - } - } - - @Override - protected void setupGUI() { - super.setupGUI(); - resetFragmentView(); - txtvTitle = (TextView) findViewById(R.id.txtvTitle); - txtvFeed = (TextView) findViewById(R.id.txtvFeed); - butNavLeft = (ImageButton) findViewById(R.id.butNavLeft); - butNavRight = (ImageButton) findViewById(R.id.butNavRight); - butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed); - - butNavLeft.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - if (currentlyShownFragment == null - || currentlyShownPosition == POS_DESCR) { - switchToFragment(POS_COVER); - } else if (currentlyShownPosition == POS_COVER) { - switchToFragment(POS_DESCR); - } else if (currentlyShownPosition == POS_CHAPTERS) { - switchToFragment(POS_COVER); - } - } - }); - - butNavRight.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - if (currentlyShownPosition == POS_CHAPTERS) { - switchToFragment(POS_DESCR); - } else { - switchToFragment(POS_CHAPTERS); - } - } - }); - - butPlaybackSpeed.setOnClickListener(new OnClickListener() { + private static final int POS_COVER = 0; + private static final int POS_DESCR = 1; + private static final int POS_CHAPTERS = 2; + private static final int NUM_CONTENT_FRAGMENTS = 3; + + final String TAG = "AudioplayerActivity"; + private static final String PREFS = "AudioPlayerActivityPreferences"; + private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition"; + private static final String PREF_PLAYABLE_ID = "playableId"; + + private Fragment[] detachedFragments; + + private CoverFragment coverFragment; + private ItemDescriptionFragment descriptionFragment; + private ListFragment chapterFragment; + + private Fragment currentlyShownFragment; + private int currentlyShownPosition = -1; + /** + * Used if onResume was called without loadMediaInfo. + */ + private int savedPosition = -1; + + private TextView txtvTitle; + private TextView txtvFeed; + private Button butPlaybackSpeed; + private ImageButton butNavLeft; + private ImageButton butNavRight; + + private void resetFragmentView() { + FragmentTransaction fT = getSupportFragmentManager().beginTransaction(); + + if (coverFragment != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Removing cover fragment"); + fT.remove(coverFragment); + } + if (descriptionFragment != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Removing description fragment"); + fT.remove(descriptionFragment); + } + if (chapterFragment != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Removing chapter fragment"); + fT.remove(chapterFragment); + } + if (currentlyShownFragment != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Removing currently shown fragment"); + fT.remove(currentlyShownFragment); + } + for (int i = 0; i < detachedFragments.length; i++) { + Fragment f = detachedFragments[i]; + if (f != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Removing detached fragment"); + fT.remove(f); + } + } + fT.commit(); + currentlyShownFragment = null; + coverFragment = null; + descriptionFragment = null; + chapterFragment = null; + currentlyShownPosition = -1; + detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS]; + } + + @Override + protected void onStop() { + super.onStop(); + if (AppConfig.DEBUG) + Log.d(TAG, "onStop"); + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayShowTitleEnabled(false); + detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS]; + } + + private void savePreferences() { + if (AppConfig.DEBUG) + Log.d(TAG, "Saving preferences"); + SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + if (currentlyShownPosition >= 0 && controller != null + && controller.getMedia() != null) { + editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, + currentlyShownPosition); + editor.putString(PREF_PLAYABLE_ID, controller.getMedia() + .getIdentifier().toString()); + } else { + editor.putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1); + editor.putString(PREF_PLAYABLE_ID, ""); + } + editor.commit(); + + savedPosition = currentlyShownPosition; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + // super.onSaveInstanceState(outState); would cause crash + if (AppConfig.DEBUG) + Log.d(TAG, "onSaveInstanceState"); + } + + @Override + protected void onPause() { + savePreferences(); + resetFragmentView(); + super.onPause(); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + restoreFromPreferences(); + } + + /** + * Tries to restore the selected fragment position from the Activity's + * preferences. + * + * @return true if restoreFromPrefernces changed the activity's state + */ + private boolean restoreFromPreferences() { + if (AppConfig.DEBUG) + Log.d(TAG, "Restoring instance state"); + SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); + int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, + -1); + String playableId = prefs.getString(PREF_PLAYABLE_ID, ""); + + if (savedPosition != -1 + && controller != null + && controller.getMedia() != null + && controller.getMedia().getIdentifier().toString() + .equals(playableId)) { + switchToFragment(savedPosition); + return true; + } else if (controller == null || controller.getMedia() == null) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Couldn't restore from preferences: controller or media was null"); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, + "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: " + + savedPosition + ", id: " + playableId); + + } + return false; + } + + @Override + protected void onResume() { + super.onResume(); + if (getIntent().getAction() != null + && getIntent().getAction().equals(Intent.ACTION_VIEW)) { + Intent intent = getIntent(); + if (AppConfig.DEBUG) + Log.d(TAG, "Received VIEW intent: " + + intent.getData().getPath()); + ExternalMedia media = new ExternalMedia(intent.getData().getPath(), + MediaType.AUDIO); + Intent launchIntent = new Intent(this, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + true); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + startService(launchIntent); + } + if (savedPosition != -1) { + switchToFragment(savedPosition); + } + + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + protected void onAwaitingVideoSurface() { + if (AppConfig.DEBUG) Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player"); + startActivity(new Intent(this, VideoplayerActivity.class)); + } + + @Override + protected void postStatusMsg(int resId) { + setSupportProgressBarIndeterminateVisibility(resId == R.string.player_preparing_msg + || resId == R.string.player_seeking_msg + || resId == R.string.player_buffering_msg); + } + + @Override + protected void clearStatusMsg() { + setSupportProgressBarIndeterminateVisibility(false); + } + + /** + * Changes the currently displayed fragment. + * + * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS + */ + private void switchToFragment(int pos) { + if (AppConfig.DEBUG) + Log.d(TAG, "Switching contentView to position " + pos); + if (currentlyShownPosition != pos && controller != null) { + Playable media = controller.getMedia(); + if (media != null) { + FragmentTransaction ft = getSupportFragmentManager() + .beginTransaction(); + if (currentlyShownFragment != null) { + detachedFragments[currentlyShownPosition] = currentlyShownFragment; + ft.detach(currentlyShownFragment); + } + switch (pos) { + case POS_COVER: + if (coverFragment == null) { + Log.i(TAG, "Using new coverfragment"); + coverFragment = CoverFragment.newInstance(media); + } + currentlyShownFragment = coverFragment; + break; + case POS_DESCR: + if (descriptionFragment == null) { + descriptionFragment = ItemDescriptionFragment + .newInstance(media, true); + } + currentlyShownFragment = descriptionFragment; + break; + case POS_CHAPTERS: + if (chapterFragment == null) { + chapterFragment = new ListFragment() { + + @Override + public void onListItemClick(ListView l, View v, + int position, long id) { + super.onListItemClick(l, v, position, id); + Chapter chapter = (Chapter) this + .getListAdapter().getItem(position); + controller.seekToChapter(chapter); + } + + }; + chapterFragment.setListAdapter(new ChapterListAdapter( + AudioplayerActivity.this, 0, media + .getChapters(), media)); + } + currentlyShownFragment = chapterFragment; + break; + } + if (currentlyShownFragment != null) { + currentlyShownPosition = pos; + if (detachedFragments[pos] != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Reattaching fragment at position " + + pos); + ft.attach(detachedFragments[pos]); + } else { + ft.add(R.id.contentView, currentlyShownFragment); + } + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + ft.disallowAddToBackStack(); + ft.commit(); + updateNavButtonDrawable(); + } + } + } + } + + private void updateNavButtonDrawable() { + + final int[] buttonTexts = new int[] {R.string.show_shownotes_label, + R.string.show_chapters_label, R.string.show_cover_label}; + + final TypedArray drawables = obtainStyledAttributes(new int[]{ + R.attr.navigation_shownotes, R.attr.navigation_chapters}); + final Playable media = controller.getMedia(); + if (butNavLeft != null && butNavRight != null && media != null) { + switch (currentlyShownPosition) { + case POS_COVER: + butNavLeft.setScaleType(ScaleType.CENTER); + butNavLeft.setImageDrawable(drawables.getDrawable(0)); + butNavLeft.setContentDescription(getString(buttonTexts[0])); + + butNavRight.setImageDrawable(drawables.getDrawable(1)); + butNavRight.setContentDescription(getString(buttonTexts[1])); + + break; + case POS_DESCR: + butNavLeft.setScaleType(ScaleType.CENTER_CROP); + butNavLeft.post(new Runnable() { + + @Override + public void run() { + ImageLoader.getInstance().loadThumbnailBitmap(media, + butNavLeft); + } + }); + butNavLeft.setContentDescription(getString(buttonTexts[2])); + + butNavRight.setImageDrawable(drawables.getDrawable(1)); + butNavRight.setContentDescription(getString(buttonTexts[1])); + break; + case POS_CHAPTERS: + butNavLeft.setScaleType(ScaleType.CENTER_CROP); + butNavLeft.post(new Runnable() { + + @Override + public void run() { + ImageLoader.getInstance().loadThumbnailBitmap(media, + butNavLeft); + } + }); + butNavLeft.setContentDescription(getString(buttonTexts[2])); + + butNavRight.setImageDrawable(drawables.getDrawable(0)); + butNavRight.setContentDescription(getString(buttonTexts[0])); + break; + } + } + } + + @Override + protected void setupGUI() { + super.setupGUI(); + resetFragmentView(); + txtvTitle = (TextView) findViewById(R.id.txtvTitle); + txtvFeed = (TextView) findViewById(R.id.txtvFeed); + butNavLeft = (ImageButton) findViewById(R.id.butNavLeft); + butNavRight = (ImageButton) findViewById(R.id.butNavRight); + butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed); + + butNavLeft.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (currentlyShownFragment == null + || currentlyShownPosition == POS_DESCR) { + switchToFragment(POS_COVER); + } else if (currentlyShownPosition == POS_COVER) { + switchToFragment(POS_DESCR); + } else if (currentlyShownPosition == POS_CHAPTERS) { + switchToFragment(POS_COVER); + } + } + }); + + butNavRight.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (currentlyShownPosition == POS_CHAPTERS) { + switchToFragment(POS_DESCR); + } else { + switchToFragment(POS_CHAPTERS); + } + } + }); + + butPlaybackSpeed.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (controller != null && controller.canSetPlaybackSpeed()) { - String[] availableSpeeds = UserPreferences - .getPlaybackSpeedArray(); - String currentSpeed = UserPreferences.getPlaybackSpeed(); - - // Provide initial value in case the speed list has changed - // out from under us - // and our current speed isn't in the new list - String newSpeed; - if (availableSpeeds.length > 0) { - newSpeed = availableSpeeds[0]; + String[] availableSpeeds = UserPreferences + .getPlaybackSpeedArray(); + String currentSpeed = UserPreferences.getPlaybackSpeed(); + + // Provide initial value in case the speed list has changed + // out from under us + // and our current speed isn't in the new list + String newSpeed; + if (availableSpeeds.length > 0) { + newSpeed = availableSpeeds[0]; } else { - newSpeed = "1.0"; + newSpeed = "1.0"; } - for (int i = 0; i < availableSpeeds.length; i++) { - if (availableSpeeds[i].equals(currentSpeed)) { - if (i == availableSpeeds.length - 1) { - newSpeed = availableSpeeds[0]; - } else { - newSpeed = availableSpeeds[i + 1]; - } - break; - } - } - UserPreferences.setPlaybackSpeed(newSpeed); - controller.setPlaybackSpeed(Float.parseFloat(newSpeed)); + for (int i = 0; i < availableSpeeds.length; i++) { + if (availableSpeeds[i].equals(currentSpeed)) { + if (i == availableSpeeds.length - 1) { + newSpeed = availableSpeeds[0]; + } else { + newSpeed = availableSpeeds[i + 1]; + } + break; + } + } + UserPreferences.setPlaybackSpeed(newSpeed); + controller.setPlaybackSpeed(Float.parseFloat(newSpeed)); } } }); - butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() { + butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { VariableSpeedDialog.showDialog(AudioplayerActivity.this); return true; } }); - } - - @Override - protected void onPlaybackSpeedChange() { - super.onPlaybackSpeedChange(); - updateButPlaybackSpeed(); - } - - private void updateButPlaybackSpeed() { - if (controller == null - || (controller.getCurrentPlaybackSpeedMultiplier() == -1)) { - butPlaybackSpeed.setVisibility(View.GONE); - } else { - butPlaybackSpeed.setVisibility(View.VISIBLE); - butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed()); - } - } - - @Override - protected void onPositionObserverUpdate() { - super.onPositionObserverUpdate(); - notifyMediaPositionChanged(); - } - - @Override - protected void loadMediaInfo() { - super.loadMediaInfo(); - final Playable media = controller.getMedia(); - if (media != null) { - txtvTitle.setText(media.getEpisodeTitle()); - txtvFeed.setText(media.getFeedTitle()); - if (media.getChapters() != null) { - butNavRight.setVisibility(View.VISIBLE); - } else { - butNavRight.setVisibility(View.GONE); - } - - } - if (currentlyShownPosition == -1) { - if (!restoreFromPreferences()) { - switchToFragment(POS_COVER); - } - } - if (currentlyShownFragment instanceof AudioplayerContentFragment) { - ((AudioplayerContentFragment) currentlyShownFragment) - .onDataSetChanged(media); - } - updateButPlaybackSpeed(); - } - - public void notifyMediaPositionChanged() { - if (chapterFragment != null) { - ArrayAdapter<SimpleChapter> adapter = (ArrayAdapter<SimpleChapter>) chapterFragment - .getListAdapter(); - adapter.notifyDataSetChanged(); - } - } - - @Override - protected void onReloadNotification(int notificationCode) { - if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { - if (AppConfig.DEBUG) - Log.d(TAG, - "ReloadNotification received, switching to Videoplayer now"); - startActivity(new Intent(this, VideoplayerActivity.class)); - - } - } - - @Override - protected void onBufferStart() { - postStatusMsg(R.string.player_buffering_msg); - } - - @Override - protected void onBufferEnd() { - clearStatusMsg(); - } - - public interface AudioplayerContentFragment { - public void onDataSetChanged(Playable media); - } - - @Override - protected int getContentViewResourceId() { - return R.layout.audioplayer_activity; - } + } + + @Override + protected void onPlaybackSpeedChange() { + super.onPlaybackSpeedChange(); + updateButPlaybackSpeed(); + } + + private void updateButPlaybackSpeed() { + if (controller == null + || (controller.getCurrentPlaybackSpeedMultiplier() == -1)) { + butPlaybackSpeed.setVisibility(View.GONE); + } else { + butPlaybackSpeed.setVisibility(View.VISIBLE); + butPlaybackSpeed.setText(UserPreferences.getPlaybackSpeed()); + } + } + + @Override + protected void onPositionObserverUpdate() { + super.onPositionObserverUpdate(); + notifyMediaPositionChanged(); + } + + @Override + protected boolean loadMediaInfo() { + if (!super.loadMediaInfo()) { + return false; + } + final Playable media = controller.getMedia(); + if (media == null) { + return false; + } + txtvTitle.setText(media.getEpisodeTitle()); + txtvFeed.setText(media.getFeedTitle()); + if (media.getChapters() != null) { + butNavRight.setVisibility(View.VISIBLE); + } else { + butNavRight.setVisibility(View.GONE); + } + + + if (currentlyShownPosition == -1) { + if (!restoreFromPreferences()) { + switchToFragment(POS_COVER); + } + } + if (currentlyShownFragment instanceof AudioplayerContentFragment) { + ((AudioplayerContentFragment) currentlyShownFragment) + .onDataSetChanged(media); + } + updateButPlaybackSpeed(); + return true; + } + + public void notifyMediaPositionChanged() { + if (chapterFragment != null) { + ArrayAdapter<SimpleChapter> adapter = (ArrayAdapter<SimpleChapter>) chapterFragment + .getListAdapter(); + adapter.notifyDataSetChanged(); + } + } + + @Override + protected void onReloadNotification(int notificationCode) { + if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { + if (AppConfig.DEBUG) + Log.d(TAG, + "ReloadNotification received, switching to Videoplayer now"); + finish(); + startActivity(new Intent(this, VideoplayerActivity.class)); + + } + } + + @Override + protected void onBufferStart() { + postStatusMsg(R.string.player_buffering_msg); + } + + @Override + protected void onBufferEnd() { + clearStatusMsg(); + } + + public interface AudioplayerContentFragment { + public void onDataSetChanged(Playable media); + } + + @Override + protected int getContentViewResourceId() { + return R.layout.audioplayer_activity; + } } diff --git a/src/de/danoeh/antennapod/activity/DownloadActivity.java b/src/de/danoeh/antennapod/activity/DownloadActivity.java index 51491a286..f5986baf5 100644 --- a/src/de/danoeh/antennapod/activity/DownloadActivity.java +++ b/src/de/danoeh/antennapod/activity/DownloadActivity.java @@ -1,18 +1,9 @@ package de.danoeh.antennapod.activity; -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; import android.content.res.TypedArray; -import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; -import android.os.IBinder; -import android.support.v4.app.NavUtils; +import android.os.Handler; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.ActionBarActivity; import android.support.v7.view.ActionMode; @@ -22,17 +13,18 @@ import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemLongClickListener; - import android.widget.ListView; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadlistAdapter; +import de.danoeh.antennapod.asynctask.DownloadObserver; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.download.DownloadRequest; -import de.danoeh.antennapod.service.download.DownloadService; +import de.danoeh.antennapod.service.download.Downloader; import de.danoeh.antennapod.storage.DownloadRequester; +import java.util.List; + /** * Shows all running downloads in a list. The list objects are DownloadStatus * objects created by a DownloadObserver. @@ -49,13 +41,10 @@ public class DownloadActivity extends ActionBarActivity implements private ActionMode mActionMode; private DownloadRequest selectedDownload; - private DownloadService downloadService = null; - boolean mIsBound; - - private AsyncTask<Void, Void, Void> contentRefresher; - private ListView listview; + private DownloadObserver downloadObserver; + @Override protected void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); @@ -68,22 +57,19 @@ public class DownloadActivity extends ActionBarActivity implements Log.d(TAG, "Creating Activity"); requester = DownloadRequester.getInstance(); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + downloadObserver = new DownloadObserver(this, new Handler(), observerCallback); } @Override protected void onPause() { super.onPause(); - unbindService(mConnection); - unregisterReceiver(contentChanged); + downloadObserver.onPause(); } @Override protected void onResume() { super.onResume(); - registerReceiver(contentChanged, new IntentFilter( - DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); - bindService(new Intent(this, DownloadService.class), mConnection, 0); - startContentRefresher(); + downloadObserver.onResume(); if (dla != null) { dla.notifyDataSetChanged(); } @@ -94,72 +80,8 @@ public class DownloadActivity extends ActionBarActivity implements super.onStop(); if (AppConfig.DEBUG) Log.d(TAG, "Stopping Activity"); - stopContentRefresher(); - } - - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceDisconnected(ComponentName className) { - downloadService = null; - mIsBound = false; - Log.i(TAG, "Closed connection with DownloadService."); - } - - public void onServiceConnected(ComponentName name, IBinder service) { - downloadService = ((DownloadService.LocalBinder) service) - .getService(); - mIsBound = true; - if (AppConfig.DEBUG) - Log.d(TAG, "Connection to service established"); - dla = new DownloadlistAdapter(DownloadActivity.this, 0, - downloadService.getDownloads()); - listview.setAdapter(dla); - dla.notifyDataSetChanged(); - } - }; - - @SuppressLint("NewApi") - private void startContentRefresher() { - if (contentRefresher != null) { - contentRefresher.cancel(true); - } - contentRefresher = new AsyncTask<Void, Void, Void>() { - private static final int WAITING_INTERVAL = 1000; - - @Override - protected void onProgressUpdate(Void... values) { - super.onProgressUpdate(values); - if (dla != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing content automatically"); - dla.notifyDataSetChanged(); - } - } - - @Override - protected Void doInBackground(Void... params) { - while (!isCancelled()) { - try { - Thread.sleep(WAITING_INTERVAL); - publishProgress(); - } catch (InterruptedException e) { - return null; - } - } - return null; - } - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - contentRefresher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - contentRefresher.execute(); - } } - private void stopContentRefresher() { - if (contentRefresher != null) { - contentRefresher.cancel(true); - } - } @Override protected void onPostCreate(Bundle savedInstanceState) { @@ -240,31 +162,29 @@ public class DownloadActivity extends ActionBarActivity implements return handled; } - private boolean actionModeDestroyWorkaround = false; // TODO remove this workaround - private boolean skipWorkAround = Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH; - @Override public void onDestroyActionMode(ActionMode mode) { - if (skipWorkAround || actionModeDestroyWorkaround) { - mActionMode = null; - selectedDownload = null; - dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE); - actionModeDestroyWorkaround = false; - } else { - actionModeDestroyWorkaround = true; - } + mActionMode = null; + selectedDownload = null; + dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE); } - private BroadcastReceiver contentChanged = new BroadcastReceiver() { + private DownloadObserver.Callback observerCallback = new DownloadObserver.Callback() { @Override - public void onReceive(Context context, Intent intent) { + public void onContentChanged() { if (dla != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing content"); dla.notifyDataSetChanged(); } } + + @Override + public void onDownloadDataAvailable(List<Downloader> downloaderList) { + dla = new DownloadlistAdapter(DownloadActivity.this, 0, + downloaderList); + listview.setAdapter(dla); + dla.notifyDataSetChanged(); + } }; } diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java index f373bc35b..9edb312de 100644 --- a/src/de/danoeh/antennapod/activity/MainActivity.java +++ b/src/de/danoeh/antennapod/activity/MainActivity.java @@ -29,7 +29,7 @@ import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.FeedlistFragment; import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.service.download.DownloadService; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBTasks; diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java index 748a049a6..27ac7afd8 100644 --- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -16,13 +16,14 @@ import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.dialog.TimeDialog; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; +import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.ShareUtils; import de.danoeh.antennapod.util.StorageUtils; @@ -35,427 +36,442 @@ import de.danoeh.antennapod.util.playback.PlaybackController; * files. */ public abstract class MediaplayerActivity extends ActionBarActivity - implements OnSeekBarChangeListener { - private static final String TAG = "MediaplayerActivity"; - - protected PlaybackController controller; - - protected TextView txtvPosition; - protected TextView txtvLength; - protected SeekBar sbPosition; - protected ImageButton butPlay; - protected ImageButton butRev; - protected ImageButton butFF; - - private PlaybackController newPlaybackController() { - return new PlaybackController(this, false) { - - @Override - public void setupGUI() { - MediaplayerActivity.this.setupGUI(); - } - - @Override - public void onPositionObserverUpdate() { - MediaplayerActivity.this.onPositionObserverUpdate(); - } - - @Override - public void onBufferStart() { - MediaplayerActivity.this.onBufferStart(); - } - - @Override - public void onBufferEnd() { - MediaplayerActivity.this.onBufferEnd(); - } - - @Override - public void onBufferUpdate(float progress) { - MediaplayerActivity.this.onBufferUpdate(progress); - } - - @Override - public void handleError(int code) { - MediaplayerActivity.this.handleError(code); - } - - @Override - public void onReloadNotification(int code) { - MediaplayerActivity.this.onReloadNotification(code); - } - - @Override - public void onSleepTimerUpdate() { - supportInvalidateOptionsMenu(); - } - - @Override - public ImageButton getPlayButton() { - return butPlay; - } - - @Override - public void postStatusMsg(int msg) { - MediaplayerActivity.this.postStatusMsg(msg); - } - - @Override - public void clearStatusMsg() { - MediaplayerActivity.this.clearStatusMsg(); - } - - @Override - public void loadMediaInfo() { - MediaplayerActivity.this.loadMediaInfo(); - } - - @Override - public void onAwaitingVideoSurface() { - MediaplayerActivity.this.onAwaitingVideoSurface(); - } - - @Override - public void onServiceQueried() { - MediaplayerActivity.this.onServiceQueried(); - } - - @Override - public void onShutdownNotification() { - finish(); - } - - @Override - public void onPlaybackEnd() { - finish(); - } - - @Override - public void onPlaybackSpeedChange() { - MediaplayerActivity.this.onPlaybackSpeedChange(); - } - }; - - } - - protected void onPlaybackSpeedChange() { - - } - - protected void onServiceQueried() { - supportInvalidateOptionsMenu(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - if (AppConfig.DEBUG) - Log.d(TAG, "Creating Activity"); - StorageUtils.checkStorageAvailability(this); + implements OnSeekBarChangeListener { + private static final String TAG = "MediaplayerActivity"; + + protected PlaybackController controller; + + protected TextView txtvPosition; + protected TextView txtvLength; + protected SeekBar sbPosition; + protected ImageButton butPlay; + protected ImageButton butRev; + protected ImageButton butFF; + + private PlaybackController newPlaybackController() { + return new PlaybackController(this, false) { + + @Override + public void setupGUI() { + MediaplayerActivity.this.setupGUI(); + } + + @Override + public void onPositionObserverUpdate() { + MediaplayerActivity.this.onPositionObserverUpdate(); + } + + @Override + public void onBufferStart() { + MediaplayerActivity.this.onBufferStart(); + } + + @Override + public void onBufferEnd() { + MediaplayerActivity.this.onBufferEnd(); + } + + @Override + public void onBufferUpdate(float progress) { + MediaplayerActivity.this.onBufferUpdate(progress); + } + + @Override + public void handleError(int code) { + MediaplayerActivity.this.handleError(code); + } + + @Override + public void onReloadNotification(int code) { + MediaplayerActivity.this.onReloadNotification(code); + } + + @Override + public void onSleepTimerUpdate() { + supportInvalidateOptionsMenu(); + } + + @Override + public ImageButton getPlayButton() { + return butPlay; + } + + @Override + public void postStatusMsg(int msg) { + MediaplayerActivity.this.postStatusMsg(msg); + } + + @Override + public void clearStatusMsg() { + MediaplayerActivity.this.clearStatusMsg(); + } + + @Override + public boolean loadMediaInfo() { + return MediaplayerActivity.this.loadMediaInfo(); + } + + @Override + public void onAwaitingVideoSurface() { + MediaplayerActivity.this.onAwaitingVideoSurface(); + } + + @Override + public void onServiceQueried() { + MediaplayerActivity.this.onServiceQueried(); + } + + @Override + public void onShutdownNotification() { + finish(); + } + + @Override + public void onPlaybackEnd() { + finish(); + } + + @Override + public void onPlaybackSpeedChange() { + MediaplayerActivity.this.onPlaybackSpeedChange(); + } + }; + + } + + protected void onPlaybackSpeedChange() { + + } + + protected void onServiceQueried() { + supportInvalidateOptionsMenu(); + } + + protected void chooseTheme() { + setTheme(UserPreferences.getTheme()); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + chooseTheme(); + super.onCreate(savedInstanceState); + if (AppConfig.DEBUG) + Log.d(TAG, "Creating Activity"); + StorageUtils.checkStorageAvailability(this); setVolumeControlStream(AudioManager.STREAM_MUSIC); orientation = getResources().getConfiguration().orientation; - getWindow().setFormat(PixelFormat.TRANSPARENT); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - protected void onPause() { - super.onPause(); - controller.reinitServiceIfPaused(); - controller.pause(); - } - - /** - * Should be used to switch to another player activity if the mime type is - * not the correct one for the current activity. - */ - protected abstract void onReloadNotification(int notificationCode); - - /** - * Should be used to inform the user that the PlaybackService is currently - * buffering. - */ - protected abstract void onBufferStart(); - - /** - * Should be used to hide the view that was showing the 'buffering'-message. - */ - protected abstract void onBufferEnd(); - - protected void onBufferUpdate(float progress) { - if (sbPosition != null) { - sbPosition.setSecondaryProgress((int) progress - * sbPosition.getMax()); - } - } - - /** Current screen orientation. */ - protected int orientation; - - @Override - protected void onStart() { - super.onStart(); - if (controller != null) { - controller.release(); - } - controller = newPlaybackController(); - } - - @Override - protected void onStop() { - super.onStop(); - if (AppConfig.DEBUG) - Log.d(TAG, "Activity stopped"); - if (controller != null) { - controller.release(); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (AppConfig.DEBUG) - Log.d(TAG, "Activity destroyed"); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { + getWindow().setFormat(PixelFormat.TRANSPARENT); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + protected void onPause() { + super.onPause(); + controller.reinitServiceIfPaused(); + controller.pause(); + } + + /** + * Should be used to switch to another player activity if the mime type is + * not the correct one for the current activity. + */ + protected abstract void onReloadNotification(int notificationCode); + + /** + * Should be used to inform the user that the PlaybackService is currently + * buffering. + */ + protected abstract void onBufferStart(); + + /** + * Should be used to hide the view that was showing the 'buffering'-message. + */ + protected abstract void onBufferEnd(); + + protected void onBufferUpdate(float progress) { + if (sbPosition != null) { + sbPosition.setSecondaryProgress((int) progress + * sbPosition.getMax()); + } + } + + /** + * Current screen orientation. + */ + protected int orientation; + + @Override + protected void onStart() { + super.onStart(); + if (controller != null) { + controller.release(); + } + controller = newPlaybackController(); + } + + @Override + protected void onStop() { + super.onStop(); + if (AppConfig.DEBUG) + Log.d(TAG, "Activity stopped"); + if (controller != null) { + controller.release(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (AppConfig.DEBUG) + Log.d(TAG, "Activity destroyed"); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.mediaplayer, menu); - return true; - } + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mediaplayer, menu); + return true; + } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { + @Override + public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - Playable media = controller.getMedia(); - - menu.findItem(R.id.support_item).setVisible( - media != null && media.getPaymentLink() != null); - menu.findItem(R.id.share_link_item).setVisible( - media != null && media.getWebsiteLink() != null); - menu.findItem(R.id.visit_website_item).setVisible( - media != null && media.getWebsiteLink() != null); - menu.findItem(R.id.skip_episode_item).setVisible(media != null); - boolean sleepTimerSet = controller.sleepTimerActive(); - boolean sleepTimerNotSet = controller.sleepTimerNotActive(); - menu.findItem(R.id.set_sleeptimer_item).setVisible(sleepTimerNotSet); - menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerSet); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - Playable media = controller.getMedia(); - if (item.getItemId() == android.R.id.home) { - Intent intent = new Intent(MediaplayerActivity.this, - MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - return true; - } else if (media != null) { - switch (item.getItemId()) { - case R.id.disable_sleeptimer_item: - if (controller.serviceAvailable()) { - AlertDialog.Builder stDialog = new AlertDialog.Builder(this); - stDialog.setTitle(R.string.sleep_timer_label); - stDialog.setMessage(getString(R.string.time_left_label) - + Converter.getDurationStringLong((int) controller - .getSleepTimerTimeLeft())); - stDialog.setPositiveButton( - R.string.disable_sleeptimer_label, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, - int which) { - dialog.dismiss(); - controller.disableSleepTimer(); - } - }); - stDialog.setNegativeButton(R.string.cancel_label, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, - int which) { - dialog.dismiss(); - } - }); - stDialog.create().show(); - } - break; - case R.id.set_sleeptimer_item: - if (controller.serviceAvailable()) { - TimeDialog td = new TimeDialog(this, - R.string.set_sleeptimer_label, - R.string.set_sleeptimer_label) { - - @Override - public void onTimeEntered(long millis) { - controller.setSleepTimer(millis); - } - }; - td.show(); - break; - - } - case R.id.visit_website_item: - Uri uri = Uri.parse(media.getWebsiteLink()); - startActivity(new Intent(Intent.ACTION_VIEW, uri)); - break; - case R.id.support_item: - new FlattrClickWorker(this, media.getPaymentLink()) - .executeAsync(); - break; - case R.id.share_link_item: - ShareUtils.shareLink(this, media.getWebsiteLink()); - break; - case R.id.skip_episode_item: - sendBroadcast(new Intent( - PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); - break; - default: - return false; - - } - return true; - } else { - return false; - } - } - - @Override - protected void onResume() { - super.onResume(); - if (AppConfig.DEBUG) - Log.d(TAG, "Resuming Activity"); - StorageUtils.checkStorageAvailability(this); - controller.init(); - } - - /** - * Called by 'handleStatus()' when the PlaybackService is in the - * AWAITING_VIDEO_SURFACE state. - */ - protected abstract void onAwaitingVideoSurface(); - - protected abstract void postStatusMsg(int resId); - - protected abstract void clearStatusMsg(); - - protected void onPositionObserverUpdate() { - if (controller != null) { - int currentPosition = controller.getPosition(); - int duration = controller.getDuration(); - if (currentPosition != PlaybackService.INVALID_TIME - && duration != PlaybackService.INVALID_TIME - && controller.getMedia() != null) { - controller.getMedia().setPosition(currentPosition); - txtvPosition.setText(Converter - .getDurationStringLong(currentPosition)); - txtvLength.setText(Converter.getDurationStringLong(duration)); - updateProgressbarPosition(currentPosition, duration); - } else { - Log.w(TAG, - "Could not react to position observer update because of invalid time"); - } - } - } - - private void updateProgressbarPosition(int position, int duration) { - if (AppConfig.DEBUG) - Log.d(TAG, "Updating progressbar info"); - float progress = ((float) position) / duration; - sbPosition.setProgress((int) (progress * sbPosition.getMax())); - } - - /** - * Load information about the media that is going to be played or currently - * being played. This method will be called when the activity is connected - * to the PlaybackService to ensure that the activity has the right - * FeedMedia object. - */ - protected void loadMediaInfo() { - if (AppConfig.DEBUG) - Log.d(TAG, "Loading media info"); - Playable media = controller.getMedia(); - if (media != null) { - txtvPosition.setText(Converter.getDurationStringLong((media - .getPosition()))); - - if (media.getDuration() != 0) { - txtvLength.setText(Converter.getDurationStringLong(media - .getDuration())); - float progress = ((float) media.getPosition()) - / media.getDuration(); - sbPosition.setProgress((int) (progress * sbPosition.getMax())); - } - } - } - - protected void setupGUI() { - setContentView(getContentViewResourceId()); - sbPosition = (SeekBar) findViewById(R.id.sbPosition); - txtvPosition = (TextView) findViewById(R.id.txtvPosition); - txtvLength = (TextView) findViewById(R.id.txtvLength); - butPlay = (ImageButton) findViewById(R.id.butPlay); - butRev = (ImageButton) findViewById(R.id.butRev); - butFF = (ImageButton) findViewById(R.id.butFF); - - // SEEKBAR SETUP - - sbPosition.setOnSeekBarChangeListener(this); - - // BUTTON SETUP - - butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); - - butFF.setOnClickListener(controller.newOnFFButtonClickListener()); - - butRev.setOnClickListener(controller.newOnRevButtonClickListener()); - - } - - protected abstract int getContentViewResourceId(); - - void handleError(int errorCode) { - final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this); - errorDialog.setTitle(R.string.error_label); - errorDialog - .setMessage(MediaPlayerError.getErrorString(this, errorCode)); - errorDialog.setNeutralButton("OK", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - finish(); - } - }); - errorDialog.create().show(); - } - - float prog; - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, - boolean fromUser) { - prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, - txtvPosition); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - controller.onSeekBarStartTrackingTouch(seekBar); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - controller.onSeekBarStopTrackingTouch(seekBar, prog); - } + Playable media = controller.getMedia(); + + menu.findItem(R.id.support_item).setVisible( + media != null && media.getPaymentLink() != null && + (media instanceof FeedMedia) && + ((FeedMedia) media).getItem().getFlattrStatus().flattrable()); + menu.findItem(R.id.share_link_item).setVisible( + media != null && media.getWebsiteLink() != null); + menu.findItem(R.id.visit_website_item).setVisible( + media != null && media.getWebsiteLink() != null); + menu.findItem(R.id.skip_episode_item).setVisible(media != null); + boolean sleepTimerSet = controller.sleepTimerActive(); + boolean sleepTimerNotSet = controller.sleepTimerNotActive(); + menu.findItem(R.id.set_sleeptimer_item).setVisible(sleepTimerNotSet); + menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerSet); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Playable media = controller.getMedia(); + if (item.getItemId() == android.R.id.home) { + Intent intent = new Intent(MediaplayerActivity.this, + MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } else if (media != null) { + switch (item.getItemId()) { + case R.id.disable_sleeptimer_item: + if (controller.serviceAvailable()) { + AlertDialog.Builder stDialog = new AlertDialog.Builder(this); + stDialog.setTitle(R.string.sleep_timer_label); + stDialog.setMessage(getString(R.string.time_left_label) + + Converter.getDurationStringLong((int) controller + .getSleepTimerTimeLeft())); + stDialog.setPositiveButton( + R.string.disable_sleeptimer_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + controller.disableSleepTimer(); + } + }); + stDialog.setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + } + }); + stDialog.create().show(); + } + break; + case R.id.set_sleeptimer_item: + if (controller.serviceAvailable()) { + TimeDialog td = new TimeDialog(this, + R.string.set_sleeptimer_label, + R.string.set_sleeptimer_label) { + + @Override + public void onTimeEntered(long millis) { + controller.setSleepTimer(millis); + } + }; + td.show(); + break; + + } + case R.id.visit_website_item: + Uri uri = Uri.parse(media.getWebsiteLink()); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + if (media instanceof FeedMedia) { + FeedItem feedItem = ((FeedMedia) media).getItem(); + DBTasks.flattrItemIfLoggedIn(this, feedItem); + } + break; + case R.id.share_link_item: + ShareUtils.shareLink(this, media.getWebsiteLink()); + break; + case R.id.skip_episode_item: + sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + default: + return false; + + } + return true; + } else { + return false; + } + } + + @Override + protected void onResume() { + super.onResume(); + if (AppConfig.DEBUG) + Log.d(TAG, "Resuming Activity"); + StorageUtils.checkStorageAvailability(this); + controller.init(); + } + + /** + * Called by 'handleStatus()' when the PlaybackService is waiting for + * a video surface. + */ + protected abstract void onAwaitingVideoSurface(); + + protected abstract void postStatusMsg(int resId); + + protected abstract void clearStatusMsg(); + + protected void onPositionObserverUpdate() { + if (controller != null) { + int currentPosition = controller.getPosition(); + int duration = controller.getDuration(); + if (currentPosition != PlaybackService.INVALID_TIME + && duration != PlaybackService.INVALID_TIME + && controller.getMedia() != null) { + txtvPosition.setText(Converter + .getDurationStringLong(currentPosition)); + txtvLength.setText(Converter.getDurationStringLong(duration)); + updateProgressbarPosition(currentPosition, duration); + } else { + Log.w(TAG, + "Could not react to position observer update because of invalid time"); + } + } + } + + private void updateProgressbarPosition(int position, int duration) { + if (AppConfig.DEBUG) + Log.d(TAG, "Updating progressbar info"); + float progress = ((float) position) / duration; + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } + + /** + * Load information about the media that is going to be played or currently + * being played. This method will be called when the activity is connected + * to the PlaybackService to ensure that the activity has the right + * FeedMedia object. + */ + protected boolean loadMediaInfo() { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading media info"); + Playable media = controller.getMedia(); + if (media != null) { + txtvPosition.setText(Converter.getDurationStringLong((media + .getPosition()))); + + if (media.getDuration() != 0) { + txtvLength.setText(Converter.getDurationStringLong(media + .getDuration())); + float progress = ((float) media.getPosition()) + / media.getDuration(); + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } + return true; + } else { + return false; + } + } + + protected void setupGUI() { + setContentView(getContentViewResourceId()); + sbPosition = (SeekBar) findViewById(R.id.sbPosition); + txtvPosition = (TextView) findViewById(R.id.txtvPosition); + txtvLength = (TextView) findViewById(R.id.txtvLength); + butPlay = (ImageButton) findViewById(R.id.butPlay); + butRev = (ImageButton) findViewById(R.id.butRev); + butFF = (ImageButton) findViewById(R.id.butFF); + + // SEEKBAR SETUP + + sbPosition.setOnSeekBarChangeListener(this); + + // BUTTON SETUP + + butPlay.setOnClickListener(controller.newOnPlayButtonClickListener()); + + if (butFF != null) { + butFF.setOnClickListener(controller.newOnFFButtonClickListener()); + } + if (butRev != null) { + butRev.setOnClickListener(controller.newOnRevButtonClickListener()); + } + + } + + protected abstract int getContentViewResourceId(); + + void handleError(int errorCode) { + final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this); + errorDialog.setTitle(R.string.error_label); + errorDialog + .setMessage(MediaPlayerError.getErrorString(this, errorCode)); + errorDialog.setNeutralButton("OK", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + finish(); + } + }); + errorDialog.create().show(); + } + + float prog; + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromUser) { + prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, + txtvPosition); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + controller.onSeekBarStartTrackingTouch(seekBar); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + controller.onSeekBarStopTrackingTouch(seekBar, prog); + } } diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java index e6fcf5306..4a8dc1882 100644 --- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -28,7 +28,9 @@ import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.preferences.GpodnetPreferences; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import de.danoeh.antennapod.util.flattr.FlattrUtils; +import de.danoeh.antennapod.util.flattr.SimpleFlattrThing; import java.io.File; import java.util.ArrayList; @@ -44,6 +46,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { private static final String PREF_FLATTR_THIS_APP = "prefFlattrThisApp"; private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate"; private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess"; + private static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; private static final String PREF_OPML_EXPORT = "prefOpmlExport"; private static final String PREF_ABOUT = "prefAbout"; private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; @@ -78,7 +81,11 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { @Override public boolean onPreferenceClick(Preference preference) { new FlattrClickWorker(PreferenceActivity.this, - FlattrUtils.APP_URL).executeAsync(); + new SimpleFlattrThing(PreferenceActivity.this.getString(R.string.app_name), + FlattrUtils.APP_URL, + new FlattrStatus(FlattrStatus.STATUS_QUEUE) + ) + ).executeAsync(); return true; } @@ -297,6 +304,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); + findPreference(PREF_AUTO_FLATTR).setEnabled(hasFlattrToken); findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) .setEnabled(UserPreferences.isEnableAutodownload()); diff --git a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java index 01841f099..f323cb681 100644 --- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -2,289 +2,338 @@ package de.danoeh.antennapod.activity; import android.annotation.SuppressLint; import android.content.Intent; +import android.graphics.drawable.ColorDrawable; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.util.Pair; import android.view.*; +import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.SeekBar; -import android.widget.VideoView; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; -import de.danoeh.antennapod.service.PlayerStatus; +import de.danoeh.antennapod.service.playback.PlaybackService; +import de.danoeh.antennapod.service.playback.PlayerStatus; import de.danoeh.antennapod.util.playback.ExternalMedia; import de.danoeh.antennapod.util.playback.Playable; - -/** Activity for playing audio files. */ -public class VideoplayerActivity extends MediaplayerActivity implements - SurfaceHolder.Callback { - private static final String TAG = "VideoplayerActivity"; - - /** True if video controls are currently visible. */ - private boolean videoControlsShowing = true; - private boolean videoSurfaceCreated = false; - private VideoControlsHider videoControlsToggler; - - private LinearLayout videoOverlay; - private VideoView videoview; - private ProgressBar progressIndicator; - - @Override - protected void onCreate(Bundle savedInstanceState) { - requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); - setTheme(UserPreferences.getTheme()); - - super.onCreate(savedInstanceState); - } - - @Override - protected void onPause() { - super.onPause(); - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - } - - @Override - protected void onResume() { - super.onResume(); - if (getIntent().getAction() != null - && getIntent().getAction().equals(Intent.ACTION_VIEW)) { - Intent intent = getIntent(); - if (AppConfig.DEBUG) - Log.d(TAG, "Received VIEW intent: " - + intent.getData().getPath()); - ExternalMedia media = new ExternalMedia(intent.getData().getPath(), - MediaType.VIDEO); - Intent launchIntent = new Intent(this, PlaybackService.class); - launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, - true); - launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); - launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, - true); - startService(launchIntent); - } - } - - @Override - protected void loadMediaInfo() { - super.loadMediaInfo(); - Playable media = controller.getMedia(); - if (media != null) { - getSupportActionBar().setSubtitle(media.getEpisodeTitle()); - getSupportActionBar().setTitle(media.getFeedTitle()); - } - } - - @Override - protected void setupGUI() { - super.setupGUI(); - videoOverlay = (LinearLayout) findViewById(R.id.overlay); - videoview = (VideoView) findViewById(R.id.videoview); - progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator); - videoview.getHolder().addCallback(this); - videoview.setOnTouchListener(onVideoviewTouched); - - setupVideoControlsToggler(); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - @Override - protected void onAwaitingVideoSurface() { - if (videoSurfaceCreated) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Videosurface already created, setting videosurface now"); - controller.setVideoSurface(videoview.getHolder()); - } - } - - @Override - protected void postStatusMsg(int resId) { - if (resId == R.string.player_preparing_msg) { - progressIndicator.setVisibility(View.VISIBLE); - } else { - progressIndicator.setVisibility(View.INVISIBLE); - } - - } - - @Override - protected void clearStatusMsg() { - progressIndicator.setVisibility(View.INVISIBLE); - } - - View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() { - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - toggleVideoControlsVisibility(); - if (videoControlsShowing) { - setupVideoControlsToggler(); - } - - return true; - } else { - return false; - } - } - }; - - @SuppressLint("NewApi") - void setupVideoControlsToggler() { - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - videoControlsToggler = new VideoControlsHider(); - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - videoControlsToggler - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - videoControlsToggler.execute(); - } - } - - private void toggleVideoControlsVisibility() { - if (videoControlsShowing) { - getSupportActionBar().hide(); - hideVideoControls(); - } else { - getSupportActionBar().show(); - showVideoControls(); - } - videoControlsShowing = !videoControlsShowing; - } - - /** Hides the videocontrols after a certain period of time. */ - public class VideoControlsHider extends AsyncTask<Void, Void, Void> { - @Override - protected void onCancelled() { - videoControlsToggler = null; - } - - @Override - protected void onPostExecute(Void result) { - videoControlsToggler = null; - } - - private static final int WAITING_INTERVALL = 5000; - private static final String TAG = "VideoControlsToggler"; - - @Override - protected void onProgressUpdate(Void... values) { - if (videoControlsShowing) { - if (AppConfig.DEBUG) - Log.d(TAG, "Hiding video controls"); - getSupportActionBar().hide(); - hideVideoControls(); - videoControlsShowing = false; - } - } - - @Override - protected Void doInBackground(Void... params) { - try { - Thread.sleep(WAITING_INTERVALL); - } catch (InterruptedException e) { - return null; - } - publishProgress(); - return null; - } - - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, - int height) { - holder.setFixedSize(width, height); - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - if (AppConfig.DEBUG) - Log.d(TAG, "Videoview holder created"); - videoSurfaceCreated = true; - if (controller.getStatus() == PlayerStatus.AWAITING_VIDEO_SURFACE) { - if (controller.serviceAvailable()) { - controller.setVideoSurface(holder); - } else { - Log.e(TAG, - "Could'nt attach surface to mediaplayer - reference to service was null"); - } - } - - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - if (AppConfig.DEBUG) - Log.d(TAG, "Videosurface was destroyed"); - videoSurfaceCreated = false; - controller.notifyVideoSurfaceAbandoned(); - } - - @Override - protected void onReloadNotification(int notificationCode) { - if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) { - if (AppConfig.DEBUG) - Log.d(TAG, - "ReloadNotification received, switching to Audioplayer now"); - startActivity(new Intent(this, AudioplayerActivity.class)); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - super.onStartTrackingTouch(seekBar); - if (videoControlsToggler != null) { - videoControlsToggler.cancel(true); - } - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - setupVideoControlsToggler(); - } - - @Override - protected void onBufferStart() { - progressIndicator.setVisibility(View.VISIBLE); - } - - @Override - protected void onBufferEnd() { - progressIndicator.setVisibility(View.INVISIBLE); - } - - private void showVideoControls() { - videoOverlay.setVisibility(View.VISIBLE); - videoOverlay.startAnimation(AnimationUtils.loadAnimation(this, - R.anim.fade_in)); - } - - private void hideVideoControls() { - videoOverlay.startAnimation(AnimationUtils.loadAnimation(this, - R.anim.fade_out)); - videoOverlay.setVisibility(View.GONE); - } - - @Override - protected int getContentViewResourceId() { - return R.layout.videoplayer_activity; - } +import de.danoeh.antennapod.view.AspectRatioVideoView; + +/** + * Activity for playing video files. + */ +public class VideoplayerActivity extends MediaplayerActivity { + private static final String TAG = "VideoplayerActivity"; + + /** + * True if video controls are currently visible. + */ + private boolean videoControlsShowing = true; + private boolean videoSurfaceCreated = false; + private VideoControlsHider videoControlsToggler; + + private LinearLayout videoOverlay; + private AspectRatioVideoView videoview; + private ProgressBar progressIndicator; + + @Override + protected void chooseTheme() { + setTheme(R.style.Theme_AntennaPod_Dark); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (Build.VERSION.SDK_INT >= 11) { + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + } + super.onCreate(savedInstanceState); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000)); + } + + @Override + protected void onPause() { + super.onPause(); + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { + controller.pause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (getIntent().getAction() != null + && getIntent().getAction().equals(Intent.ACTION_VIEW)) { + Intent intent = getIntent(); + if (AppConfig.DEBUG) + Log.d(TAG, "Received VIEW intent: " + + intent.getData().getPath()); + ExternalMedia media = new ExternalMedia(intent.getData().getPath(), + MediaType.VIDEO); + Intent launchIntent = new Intent(this, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + true); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + startService(launchIntent); + } + } + + @Override + protected boolean loadMediaInfo() { + if (!super.loadMediaInfo()) { + return false; + } + Playable media = controller.getMedia(); + if (media != null) { + getSupportActionBar().setSubtitle(media.getEpisodeTitle()); + getSupportActionBar().setTitle(media.getFeedTitle()); + return true; + } + + return false; + } + + @Override + protected void setupGUI() { + super.setupGUI(); + videoOverlay = (LinearLayout) findViewById(R.id.overlay); + videoview = (AspectRatioVideoView) findViewById(R.id.videoview); + progressIndicator = (ProgressBar) findViewById(R.id.progressIndicator); + videoview.getHolder().addCallback(surfaceHolderCallback); + videoview.setOnTouchListener(onVideoviewTouched); + + setupVideoControlsToggler(); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + @Override + protected void onAwaitingVideoSurface() { + if (videoSurfaceCreated) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Videosurface already created, setting videosurface now"); + + Pair<Integer, Integer> videoSize = controller.getVideoSize(); + if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { + if (AppConfig.DEBUG) Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); + videoview.setVideoSize(videoSize.first, videoSize.second); + } else { + Log.e(TAG, "Could not determine video size"); + } + controller.setVideoSurface(videoview.getHolder()); + } + } + + @Override + protected void postStatusMsg(int resId) { + if (resId == R.string.player_preparing_msg) { + progressIndicator.setVisibility(View.VISIBLE); + } else { + progressIndicator.setVisibility(View.INVISIBLE); + } + + } + + @Override + protected void clearStatusMsg() { + progressIndicator.setVisibility(View.INVISIBLE); + } + + View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + toggleVideoControlsVisibility(); + if (videoControlsShowing) { + setupVideoControlsToggler(); + } + + return true; + } else { + return false; + } + } + }; + + @SuppressLint("NewApi") + void setupVideoControlsToggler() { + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + videoControlsToggler = new VideoControlsHider(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + videoControlsToggler + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + videoControlsToggler.execute(); + } + } + + private void toggleVideoControlsVisibility() { + if (videoControlsShowing) { + getSupportActionBar().hide(); + hideVideoControls(); + } else { + getSupportActionBar().show(); + showVideoControls(); + } + videoControlsShowing = !videoControlsShowing; + } + + /** + * Hides the videocontrols after a certain period of time. + */ + public class VideoControlsHider extends AsyncTask<Void, Void, Void> { + @Override + protected void onCancelled() { + videoControlsToggler = null; + } + + @Override + protected void onPostExecute(Void result) { + videoControlsToggler = null; + } + + private static final int WAITING_INTERVALL = 5000; + private static final String TAG = "VideoControlsToggler"; + + @Override + protected void onProgressUpdate(Void... values) { + if (videoControlsShowing) { + if (AppConfig.DEBUG) + Log.d(TAG, "Hiding video controls"); + getSupportActionBar().hide(); + hideVideoControls(); + videoControlsShowing = false; + } + } + + @Override + protected Void doInBackground(Void... params) { + try { + Thread.sleep(WAITING_INTERVALL); + } catch (InterruptedException e) { + return null; + } + publishProgress(); + return null; + } + + } + + private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + holder.setFixedSize(width, height); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (AppConfig.DEBUG) + Log.d(TAG, "Videoview holder created"); + videoSurfaceCreated = true; + if (controller.getStatus() == PlayerStatus.PLAYING) { + if (controller.serviceAvailable()) { + controller.setVideoSurface(holder); + } else { + Log.e(TAG, + "Could'nt attach surface to mediaplayer - reference to service was null"); + } + } + + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (AppConfig.DEBUG) + Log.d(TAG, "Videosurface was destroyed"); + videoSurfaceCreated = false; + controller.notifyVideoSurfaceAbandoned(); + } + }; + + + @Override + protected void onReloadNotification(int notificationCode) { + if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) { + if (AppConfig.DEBUG) + Log.d(TAG, + "ReloadNotification received, switching to Audioplayer now"); + finish(); + startActivity(new Intent(this, AudioplayerActivity.class)); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + super.onStartTrackingTouch(seekBar); + if (videoControlsToggler != null) { + videoControlsToggler.cancel(true); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + setupVideoControlsToggler(); + } + + @Override + protected void onBufferStart() { + progressIndicator.setVisibility(View.VISIBLE); + } + + @Override + protected void onBufferEnd() { + progressIndicator.setVisibility(View.INVISIBLE); + } + + private void showVideoControls() { + videoOverlay.setVisibility(View.VISIBLE); + butPlay.setVisibility(View.VISIBLE); + final Animation animation = AnimationUtils.loadAnimation(this, + R.anim.fade_in); + if (animation != null) { + videoOverlay.startAnimation(animation); + butPlay.startAnimation(animation); + } + if (Build.VERSION.SDK_INT >= 14) { + videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + } + + private void hideVideoControls() { + final Animation animation = AnimationUtils.loadAnimation(this, + R.anim.fade_out); + if (animation != null) { + videoOverlay.startAnimation(animation); + butPlay.startAnimation(animation); + } + if (Build.VERSION.SDK_INT >= 14) { + videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } + videoOverlay.setVisibility(View.GONE); + butPlay.setVisibility(View.GONE); + } + + @Override + protected int getContentViewResourceId() { + return R.layout.videoplayer_activity; + } } diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java index d355a7826..e5a00923a 100644 --- a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -56,9 +56,9 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTheme(UserPreferences.getTheme()); setContentView(R.layout.gpodnetauth_activity); service = new GpodnetService(); diff --git a/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java index 2b49795c3..e384ecffc 100644 --- a/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DefaultFeedItemlistAdapter.java @@ -91,10 +91,12 @@ public class DefaultFeedItemlistAdapter extends BaseAdapter { MediaType mediaType = item.getMedia().getMediaType(); if (mediaType == MediaType.AUDIO) { holder.type.setImageDrawable(typeDrawables.getDrawable(0)); + holder.type.setContentDescription(context.getString(R.string.media_type_audio_label)); holder.type.setVisibility(View.VISIBLE); } else if (mediaType == MediaType.VIDEO) { holder.type.setImageDrawable(typeDrawables.getDrawable(1)); - holder.type.setVisibility(View.VISIBLE); + holder.type.setContentDescription(context.getString(R.string.media_type_video_label)); + holder.type.setVisibility(View.VISIBLE); } else { holder.type.setImageBitmap(null); holder.type.setVisibility(View.GONE); diff --git a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java index b00066eca..aed988b59 100644 --- a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java @@ -148,12 +148,14 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { TypedArray drawables = context.obtainStyledAttributes(new int[] { R.attr.av_download, R.attr.navigation_refresh }); + final int[] labels = new int[] {R.string.status_downloaded_label, R.string.downloading_label}; holder.lenSize.setVisibility(View.VISIBLE); if (!media.isDownloaded()) { if (DownloadRequester.getInstance().isDownloadingFile(media)) { holder.downloadStatus.setVisibility(View.VISIBLE); holder.downloadStatus.setImageDrawable(drawables .getDrawable(1)); + holder.downloadStatus.setContentDescription(context.getString(labels[1])); } else { holder.downloadStatus.setVisibility(View.INVISIBLE); } @@ -161,6 +163,7 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { holder.downloadStatus.setVisibility(View.VISIBLE); holder.downloadStatus .setImageDrawable(drawables.getDrawable(0)); + holder.downloadStatus.setContentDescription(context.getString(labels[0])); } } else { holder.downloadStatus.setVisibility(View.INVISIBLE); diff --git a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java index b8bec44c8..238ae29c6 100644 --- a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java @@ -176,12 +176,16 @@ public class InternalFeedItemlistAdapter extends DefaultFeedItemlistAdapter { TypedArray typeDrawables = getContext().obtainStyledAttributes( new int[] { R.attr.type_audio, R.attr.type_video }); + final int[] labels = new int[] {R.string.media_type_audio_label, R.string.media_type_video_label}; + MediaType mediaType = item.getMedia().getMediaType(); if (mediaType == MediaType.AUDIO) { holder.type.setImageDrawable(typeDrawables.getDrawable(0)); + holder.type.setContentDescription(getContext().getString(labels[0])); holder.type.setVisibility(View.VISIBLE); } else if (mediaType == MediaType.VIDEO) { holder.type.setImageDrawable(typeDrawables.getDrawable(1)); + holder.type.setContentDescription(getContext().getString(labels[1])); holder.type.setVisibility(View.VISIBLE); } else { holder.type.setImageBitmap(null); diff --git a/src/de/danoeh/antennapod/asynctask/DownloadObserver.java b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java new file mode 100644 index 000000000..26e405615 --- /dev/null +++ b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java @@ -0,0 +1,150 @@ +package de.danoeh.antennapod.asynctask; + +import android.app.Activity; +import android.content.*; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.service.download.DownloadService; +import de.danoeh.antennapod.service.download.Downloader; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provides access to the DownloadService's list of items that are currently being downloaded. + * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause() + * should be called in the activity's onResume() and onPause() methods + */ +public class DownloadObserver { + private static final String TAG = "DownloadObserver"; + + /** + * Time period between update notifications. + */ + public static final int WAITING_INTERVAL_MS = 1000; + + private final Activity activity; + private final Handler handler; + private final Callback callback; + + private DownloadService downloadService = null; + private AtomicBoolean mIsBound = new AtomicBoolean(false); + + private Thread refresherThread; + private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false); + + + /** + * Creates a new download observer. + * + * @param activity Used for registering receivers + * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread. + * @param callback Callback methods for posting content updates + * @throws java.lang.IllegalArgumentException if one of the arguments is null. + */ + public DownloadObserver(Activity activity, Handler handler, Callback callback) { + if (activity == null) throw new IllegalArgumentException("activity = null"); + if (handler == null) throw new IllegalArgumentException("handler = null"); + if (callback == null) throw new IllegalArgumentException("callback = null"); + + this.activity = activity; + this.handler = handler; + this.callback = callback; + } + + public void onResume() { + if (AppConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed"); + activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); + activity.bindService(new Intent(activity, DownloadService.class), mConnection, 0); + } + + public void onPause() { + if (AppConfig.DEBUG) Log.d(TAG, "DownloadObserver paused"); + activity.unregisterReceiver(contentChangedReceiver); + activity.unbindService(mConnection); + stopRefresher(); + } + + private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + callback.onContentChanged(); + startRefresher(); + } + }; + + public interface Callback { + void onContentChanged(); + + void onDownloadDataAvailable(List<Downloader> downloaderList); + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceDisconnected(ComponentName className) { + downloadService = null; + mIsBound.set(false); + stopRefresher(); + Log.i(TAG, "Closed connection with DownloadService."); + } + + public void onServiceConnected(ComponentName name, IBinder service) { + downloadService = ((DownloadService.LocalBinder) service) + .getService(); + mIsBound.set(true); + if (AppConfig.DEBUG) + Log.d(TAG, "Connection to service established"); + List<Downloader> downloaderList = downloadService.getDownloads(); + if (downloaderList != null && !downloaderList.isEmpty()) { + callback.onDownloadDataAvailable(downloaderList); + startRefresher(); + } + } + }; + + private void stopRefresher() { + if (refresherThread != null) { + refresherThread.interrupt(); + } + } + + private void startRefresher() { + if (refresherThread == null || refresherThread.isInterrupted()) { + refresherThread = new Thread(new RefresherThread()); + refresherThread.start(); + } + } + + private class RefresherThread implements Runnable { + + public void run() { + refresherThreadRunning.set(true); + while (!Thread.interrupted()) { + try { + Thread.sleep(WAITING_INTERVAL_MS); + } catch (InterruptedException e) { + Log.d(TAG, "Refresher thread was interrupted"); + } + if (mIsBound.get()) { + postUpdate(); + } + } + refresherThreadRunning.set(false); + } + + private void postUpdate() { + handler.post(new Runnable() { + @Override + public void run() { + callback.onContentChanged(); + List<Downloader> downloaderList = downloadService.getDownloads(); + if (downloaderList == null || downloaderList.isEmpty()) { + Thread.currentThread().interrupt(); + } + } + }); + } + } + +} diff --git a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java index 975aa5efe..3034bbaff 100644 --- a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java +++ b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java @@ -1,115 +1,308 @@ package de.danoeh.antennapod.asynctask; -import org.shredzone.flattr4j.exception.FlattrException; - import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; import android.app.ProgressDialog; import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.AsyncTask; +import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.flattr.FlattrThing; import de.danoeh.antennapod.util.flattr.FlattrUtils; -/** Performs a click action in a background thread. */ - -public class FlattrClickWorker extends AsyncTask<Void, Void, Void> { - protected static final String TAG = "FlattrClickWorker"; - protected Context context; - protected String url; - protected String errorMsg; - protected int exitCode; - protected ProgressDialog progDialog; - - protected final static int SUCCESS = 0; - protected final static int NO_TOKEN = 1; - protected final static int FLATTR_ERROR = 2; - - public FlattrClickWorker(Context context, String url) { - super(); - this.context = context; - this.url = url; - exitCode = SUCCESS; - errorMsg = ""; - } - - protected void onNoAccessToken() { - Log.w(TAG, "No access token was available"); - if (url.equals(FlattrUtils.APP_URL)) { - FlattrUtils.showNoTokenDialog(context, FlattrUtils.APP_LINK); - } else { - FlattrUtils.showNoTokenDialog(context, url); - } - } - - protected void onFlattrError() { - FlattrUtils.showErrorDialog(context, errorMsg); - } - - protected void onSuccess() { - Toast toast = Toast.makeText(context.getApplicationContext(), - R.string.flattr_click_success, Toast.LENGTH_LONG); - toast.show(); - } - - protected void onSetupProgDialog() { - progDialog = new ProgressDialog(context); - progDialog.setMessage(context.getString(R.string.flattring_label)); - progDialog.setIndeterminate(true); - progDialog.setCancelable(false); - progDialog.show(); - } - - @Override - protected void onPostExecute(Void result) { - if (AppConfig.DEBUG) Log.d(TAG, "Exit code was " + exitCode); - if (progDialog != null) { - progDialog.dismiss(); - } - switch (exitCode) { - case NO_TOKEN: - onNoAccessToken(); - break; - case FLATTR_ERROR: - onFlattrError(); - break; - case SUCCESS: - onSuccess(); - break; - } - } - - @Override - protected void onPreExecute() { - onSetupProgDialog(); - } - - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) Log.d(TAG, "Starting background work"); - if (FlattrUtils.hasToken()) { - try { - FlattrUtils.clickUrl(context, url); - } catch (FlattrException e) { - e.printStackTrace(); - exitCode = FLATTR_ERROR; - errorMsg = e.getMessage(); - } - } else { - exitCode = NO_TOKEN; - } - return null; - } - - @SuppressLint("NewApi") - public void executeAsync() { - FlattrUtils.hasToken(); - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(THREAD_POOL_EXECUTOR); - } else { - execute(); - } - } +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Performs a click action in a background thread. + */ + +public class FlattrClickWorker extends AsyncTask<Void, String, Void> { + protected static final String TAG = "FlattrClickWorker"; + protected Context context; + + private final int NOTIFICATION_ID = 4; + + protected String errorMsg; + protected int exitCode; + protected ArrayList<String> flattrd; + protected ArrayList<String> flattr_failed; + + + protected NotificationCompat.Builder notificationCompatBuilder; + private Notification.BigTextStyle notificationBuilder; + protected NotificationManager notificationManager; + + protected ProgressDialog progDialog; + + protected final static int EXIT_DEFAULT = 0; + protected final static int NO_TOKEN = 1; + protected final static int ENQUEUED = 2; + protected final static int NO_THINGS = 3; + + public final static int ENQUEUE_ONLY = 1; + public final static int FLATTR_TOAST = 2; + public static final int FLATTR_NOTIFICATION = 3; + + private int run_mode = FLATTR_NOTIFICATION; + + private FlattrThing extra_flattr_thing; // additional urls to flattr that do *not* originate from the queue + + /** + * @param context + * @param run_mode can be one of ENQUEUE_ONLY, FLATTR_TOAST and FLATTR_NOTIFICATION + */ + public FlattrClickWorker(Context context, int run_mode) { + this(context); + this.run_mode = run_mode; + } + + public FlattrClickWorker(Context context) { + super(); + this.context = context; + exitCode = EXIT_DEFAULT; + + flattrd = new ArrayList<String>(); + flattr_failed = new ArrayList<String>(); + + errorMsg = ""; + } + + /* only used in PreferencesActivity for flattring antennapod itself, + * can't really enqueue this thing + */ + public FlattrClickWorker(Context context, FlattrThing thing) { + this(context); + extra_flattr_thing = thing; + run_mode = FLATTR_TOAST; + Log.d(TAG, "Going to flattr special thing that is not in the queue: " + thing.getTitle()); + } + + protected void onNoAccessToken() { + Log.w(TAG, "No access token was available"); + } + + protected void onFlattrError() { + FlattrUtils.showErrorDialog(context, errorMsg); + } + + protected void onFlattred() { + String notificationTitle = context.getString(R.string.flattrd_label); + String notificationText = "", notificationSubText = "", notificationBigText = ""; + + // text for successfully flattred items + if (flattrd.size() == 1) + notificationText = String.format(context.getString(R.string.flattr_click_success)); + else if (flattrd.size() > 1) // flattred pending items from queue + notificationText = String.format(context.getString(R.string.flattr_click_success_count, flattrd.size())); + + if (flattrd.size() > 0) { + String acc = ""; + for (String s : flattrd) + acc += s + '\n'; + acc = acc.substring(0, acc.length() - 2); + + notificationBigText = String.format(context.getString(R.string.flattr_click_success_queue), acc); + } + + // add text for failures + if (flattr_failed.size() > 0) { + notificationTitle = context.getString(R.string.flattrd_failed_label); + notificationText = String.format(context.getString(R.string.flattr_click_failure_count), flattr_failed.size()) + + " " + notificationText; + + notificationSubText = flattr_failed.get(0); + + String acc = ""; + for (String s : flattr_failed) + acc += s + '\n'; + acc = acc.substring(0, acc.length() - 2); + + notificationBigText = String.format(context.getString(R.string.flattr_click_failure), acc) + + "\n" + notificationBigText; + } + + Log.d(TAG, "Going to post notification: " + notificationBigText); + + notificationManager.cancel(NOTIFICATION_ID); + + if (run_mode == FLATTR_NOTIFICATION || flattr_failed.size() > 0) { + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder = new Notification.BigTextStyle( + new Notification.Builder(context) + .setOngoing(false) + .setContentTitle(notificationTitle) + .setContentText(notificationText) + .setSubText(notificationSubText) + .setSmallIcon(R.drawable.stat_notify_sync)) + .bigText(notificationText + "\n" + notificationBigText); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } else { + notificationCompatBuilder = new NotificationCompat.Builder(context) // need new notificationBuilder and cancel/renotify to get rid of progress bar + .setContentTitle(notificationTitle) + .setContentText(notificationText) + .setSubText(notificationBigText) + .setTicker(notificationTitle) + .setSmallIcon(R.drawable.stat_notify_sync) + .setOngoing(false); + notificationManager.notify(NOTIFICATION_ID, notificationCompatBuilder.build()); + } + } else if (run_mode == FLATTR_TOAST) { + Toast.makeText(context.getApplicationContext(), + notificationText, + Toast.LENGTH_LONG) + .show(); + } + } + + protected void onEnqueue() { + Toast.makeText(context.getApplicationContext(), + R.string.flattr_click_enqueued, + Toast.LENGTH_LONG) + .show(); + } + + protected void onSetupNotification() { + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder = new Notification.BigTextStyle( + new Notification.Builder(context) + .setContentTitle(context.getString(R.string.flattring_label)) + .setAutoCancel(true) + .setSmallIcon(R.drawable.stat_notify_sync) + .setProgress(0, 0, true) + .setOngoing(true)); + } else { + notificationCompatBuilder = new NotificationCompat.Builder(context) + .setContentTitle(context.getString(R.string.flattring_label)) + .setAutoCancel(true) + .setSmallIcon(R.drawable.stat_notify_sync) + .setProgress(0, 0, true) + .setOngoing(true); + } + + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + @Override + protected void onPostExecute(Void result) { + if (AppConfig.DEBUG) Log.d(TAG, "Exit code was " + exitCode); + + switch (exitCode) { + case NO_TOKEN: + notificationManager.cancel(NOTIFICATION_ID); + onNoAccessToken(); + break; + case ENQUEUED: + onEnqueue(); + break; + case EXIT_DEFAULT: + onFlattred(); + break; + case NO_THINGS: // FlattrClickWorker called automatically somewhere to empty flattr queue + notificationManager.cancel(NOTIFICATION_ID); + break; + } + } + + @Override + protected void onPreExecute() { + onSetupNotification(); + } + + private static boolean haveInternetAccess(Context context) { + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + return (networkInfo != null && networkInfo.isConnectedOrConnecting()); + } + + @Override + protected Void doInBackground(Void... params) { + if (AppConfig.DEBUG) Log.d(TAG, "Starting background work"); + + exitCode = EXIT_DEFAULT; + + if (!FlattrUtils.hasToken()) { + exitCode = NO_TOKEN; + } else if (DBReader.getFlattrQueueEmpty(context) && extra_flattr_thing == null) { + exitCode = NO_THINGS; + } else if (!haveInternetAccess(context) || run_mode == ENQUEUE_ONLY) { + exitCode = ENQUEUED; + } else { + List<FlattrThing> flattrList = DBReader.getFlattrQueue(context); + Log.d(TAG, "flattrQueue processing list with " + flattrList.size() + " items."); + + if (extra_flattr_thing != null) + flattrList.add(extra_flattr_thing); + + flattrd.ensureCapacity(flattrList.size()); + + for (FlattrThing thing : flattrList) { + try { + Log.d(TAG, "flattrQueue processing " + thing.getTitle() + " " + thing.getPaymentLink()); + publishProgress(String.format(context.getString(R.string.flattring_thing), thing.getTitle())); + + thing.getFlattrStatus().setUnflattred(); // pop from queue to prevent unflattrable things from getting stuck in flattr queue infinitely + + FlattrUtils.clickUrl(context, thing.getPaymentLink()); + flattrd.add(thing.getTitle()); + + thing.getFlattrStatus().setFlattred(); + } catch (Exception e) { + Log.d(TAG, "flattrQueue processing exception at item " + thing.getTitle() + " " + e.getMessage()); + flattr_failed.ensureCapacity(flattrList.size()); + flattr_failed.add(thing.getTitle() + ": " + e.getMessage()); + } + Log.d(TAG, "flattrQueue processing - going to write thing back to db with flattr_status " + Long.toString(thing.getFlattrStatus().toLong())); + DBWriter.setFlattredStatus(context, thing, false); + } + + } + + return null; + } + + @Override + protected void onProgressUpdate(String... names) { + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder.setBigContentTitle(names[0]); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } else { + notificationCompatBuilder.setContentText(names[0]); + notificationManager.notify(NOTIFICATION_ID, notificationCompatBuilder.build()); + } + } + + @SuppressLint("NewApi") + public void executeAsync() { + FlattrUtils.hasToken(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + + public void executeSync() { + class DirectExecutor implements Executor { + public void execute(Runnable r) { + r.run(); + } + } + FlattrUtils.hasToken(); + executeOnExecutor(new DirectExecutor()); + + } + } diff --git a/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java b/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java new file mode 100644 index 000000000..4974c6b56 --- /dev/null +++ b/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.asynctask; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.flattr.FlattrUtils; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Fetch list of flattred things and flattr status in database in a background thread. + */ + +public class FlattrStatusFetcher extends Thread { + protected static final String TAG = "FlattrStatusFetcher"; + protected Context context; + + public FlattrStatusFetcher(Context context) { + super(); + this.context = context; + } + + @Override + public void run() { + if (AppConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status"); + + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + + try { + List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings(); + DBWriter.setFlattredStatus(context, flattredThings).get(); + } catch (FlattrException e) { + e.printStackTrace(); + Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + if (AppConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status"); + } +} diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java index a99213dc7..994446f43 100644 --- a/src/de/danoeh/antennapod/feed/Feed.java +++ b/src/de/danoeh/antennapod/feed/Feed.java @@ -10,13 +10,15 @@ import java.util.List; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.EpisodeFilter; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; /** * Data Object for a whole feed * * @author daniel */ -public class Feed extends FeedFile { +public class Feed extends FeedFile implements FlattrThing { public static final int FEEDFILETYPE_FEED = 0; public static final String TYPE_RSS2 = "rss"; public static final String TYPE_RSS091 = "rss"; @@ -43,6 +45,7 @@ public class Feed extends FeedFile { * Date of last refresh. */ private Date lastUpdate; + private FlattrStatus flattrStatus; private String paymentLink; /** * Feed type, for example RSS 2 or Atom @@ -59,7 +62,7 @@ public class Feed extends FeedFile { */ public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink, String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, - String downloadUrl, boolean downloaded) { + String downloadUrl, boolean downloaded, FlattrStatus status) { super(fileUrl, downloadUrl, downloaded); this.id = id; this.title = title; @@ -76,17 +79,29 @@ public class Feed extends FeedFile { this.type = type; this.feedIdentifier = feedIdentifier; this.image = image; + this.flattrStatus = status; items = new ArrayList<FeedItem>(); } /** + * This constructor is used for test purposes and uses a default flattr status object. + */ + public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink, + String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, + String downloadUrl, boolean downloaded) { + this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image, + fileUrl, downloadUrl, downloaded, new FlattrStatus()); + } + + /** * This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized. */ public Feed() { super(); items = new ArrayList<FeedItem>(); lastUpdate = new Date(); + this.flattrStatus = new FlattrStatus(); } /** @@ -96,6 +111,7 @@ public class Feed extends FeedFile { public Feed(String url, Date lastUpdate) { super(null, url, false); this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + this.flattrStatus = new FlattrStatus(); } /** @@ -105,6 +121,7 @@ public class Feed extends FeedFile { public Feed(String url, Date lastUpdate, String title) { this(url, lastUpdate); this.title = title; + this.flattrStatus = new FlattrStatus(); } /** @@ -238,6 +255,9 @@ public class Feed extends FeedFile { if (other.paymentLink != null) { paymentLink = other.paymentLink; } + if (other.flattrStatus != null) { + flattrStatus = other.flattrStatus; + } } public boolean compareWithOther(Feed other) { @@ -342,6 +362,14 @@ public class Feed extends FeedFile { this.feedIdentifier = feedIdentifier; } + public void setFlattrStatus(FlattrStatus status) { + this.flattrStatus = status; + } + + public FlattrStatus getFlattrStatus() { + return flattrStatus; + } + public String getPaymentLink() { return paymentLink; } diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java index a80460ece..f63b5beb4 100644 --- a/src/de/danoeh/antennapod/feed/FeedItem.java +++ b/src/de/danoeh/antennapod/feed/FeedItem.java @@ -10,6 +10,8 @@ import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.util.ShownotesProvider; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; /** * Data Object for a XML message @@ -17,7 +19,7 @@ import de.danoeh.antennapod.util.ShownotesProvider; * @author daniel */ public class FeedItem extends FeedComponent implements - ImageLoader.ImageWorkerTaskResource, ShownotesProvider { + ImageLoader.ImageWorkerTaskResource, ShownotesProvider, FlattrThing { /** * The id/guid that can be found in the rss/atom feed. Might not be set. @@ -42,10 +44,12 @@ public class FeedItem extends FeedComponent implements private boolean read; private String paymentLink; + private FlattrStatus flattrStatus; private List<Chapter> chapters; public FeedItem() { this.read = true; + this.flattrStatus = new FlattrStatus(); } /** @@ -59,6 +63,7 @@ public class FeedItem extends FeedComponent implements this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null; this.read = read; this.feed = feed; + this.flattrStatus = new FlattrStatus(); } public void updateFromOther(FeedItem other) { @@ -80,7 +85,7 @@ public class FeedItem extends FeedComponent implements } if (other.media != null) { if (media == null) { - media = other.media; + setMedia(other.media); } else if (media.compareWithOther(other)) { media.updateFromOther(other); } @@ -102,9 +107,9 @@ public class FeedItem extends FeedComponent implements * of the entry. */ public String getIdentifyingValue() { - if (itemIdentifier != null) { + if (itemIdentifier != null && !itemIdentifier.isEmpty()) { return itemIdentifier; - } else if (title != null) { + } else if (title != null && !title.isEmpty()) { return title; } else { return link; @@ -195,7 +200,15 @@ public class FeedItem extends FeedComponent implements this.contentEncoded = contentEncoded; } - public String getPaymentLink() { + public void setFlattrStatus(FlattrStatus status) { + this.flattrStatus = status; + } + + public FlattrStatus getFlattrStatus() { + return flattrStatus; + } + + public String getPaymentLink() { return paymentLink; } diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java index 492867983..fe2c3d17e 100644 --- a/src/de/danoeh/antennapod/feed/FeedMedia.java +++ b/src/de/danoeh/antennapod/feed/FeedMedia.java @@ -1,15 +1,11 @@ package de.danoeh.antennapod.feed; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; - import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.DBReader; @@ -17,7 +13,14 @@ import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.ChapterUtils; import de.danoeh.antennapod.util.playback.Playable; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + public class FeedMedia extends FeedFile implements Playable { + private static final String TAG = "FeedMedia"; public static final int FEEDFILETYPE_FEEDMEDIA = 2; public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; @@ -27,6 +30,7 @@ public class FeedMedia extends FeedFile implements Playable { private int duration; private int position; // Current position in file + private int played_duration; // How many ms of this file have been played (for autoflattring) private long size; // File size in Byte private String mime_type; private volatile FeedItem item; @@ -45,12 +49,13 @@ public class FeedMedia extends FeedFile implements Playable { public FeedMedia(long id, FeedItem item, int duration, int position, long size, String mime_type, String file_url, String download_url, - boolean downloaded, Date playbackCompletionDate) { + boolean downloaded, Date playbackCompletionDate, int played_duration) { super(file_url, download_url, downloaded); this.id = id; this.item = item; this.duration = duration; this.position = position; + this.played_duration = played_duration; this.size = size; this.mime_type = mime_type; this.playbackCompletionDate = playbackCompletionDate == null @@ -137,6 +142,14 @@ public class FeedMedia extends FeedFile implements Playable { this.duration = duration; } + public int getPlayedDuration() { + return played_duration; + } + + public void setPlayedDuration(int played_duration) { + this.played_duration = played_duration; + } + public int getPosition() { return position; } @@ -169,7 +182,7 @@ public class FeedMedia extends FeedFile implements Playable { * Sets the item object of this FeedMedia. If the given * FeedItem object is not null, it's 'media'-attribute value * will also be set to this media object. - * */ + */ public void setItem(FeedItem item) { this.item = item; if (item != null && item.getMedia() != this) { @@ -179,7 +192,8 @@ public class FeedMedia extends FeedFile implements Playable { public Date getPlaybackCompletionDate() { return playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); } + ? null : (Date) playbackCompletionDate.clone(); + } public void setPlaybackCompletionDate(Date playbackCompletionDate) { this.playbackCompletionDate = playbackCompletionDate == null @@ -215,6 +229,7 @@ public class FeedMedia extends FeedFile implements Playable { dest.writeString(download_url); dest.writeByte((byte) ((downloaded) ? 1 : 0)); dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); + dest.writeInt(played_duration); } @Override @@ -313,7 +328,7 @@ public class FeedMedia extends FeedFile implements Playable { @Override public void saveCurrentPosition(SharedPreferences pref, int newPosition) { - position = newPosition; + setPosition(newPosition); DBWriter.setFeedMediaPlaybackInformation(PodcastApp.getInstance(), this); } @@ -358,7 +373,7 @@ public class FeedMedia extends FeedFile implements Playable { final long id = in.readLong(); final long itemID = in.readLong(); FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(), - in.readString(), in.readByte() != 0, new Date(in.readLong())); + in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt()); result.itemID = itemID; return result; } diff --git a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index 3f967bbbe..56e6ee4b8 100644 --- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -14,7 +14,7 @@ import android.widget.TextView; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.playback.Playable; import de.danoeh.antennapod.util.playback.PlaybackController; @@ -137,10 +137,12 @@ public class ExternalPlayerFragment extends Fragment { } @Override - public void loadMediaInfo() { + public boolean loadMediaInfo() { ExternalPlayerFragment fragment = ExternalPlayerFragment.this; if (fragment != null) { - fragment.loadMediaInfo(); + return fragment.loadMediaInfo(); + } else { + return false; } } @@ -209,7 +211,7 @@ public class ExternalPlayerFragment extends Fragment { } } - private void loadMediaInfo() { + private boolean loadMediaInfo() { if (AppConfig.DEBUG) Log.d(TAG, "Loading media info"); if (controller.serviceAvailable()) { @@ -230,13 +232,16 @@ public class ExternalPlayerFragment extends Fragment { } else { butPlay.setVisibility(View.VISIBLE); } + return true; } else { Log.w(TAG, "loadMediaInfo was called while the media object of playbackService was null!"); + return false; } } else { Log.w(TAG, "loadMediaInfo was called while playbackService was null!"); + return false; } } diff --git a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java index ed607b279..d0e07b194 100644 --- a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java @@ -244,19 +244,11 @@ public class FeedlistFragment extends Fragment implements return true; } - private boolean actionModeDestroyWorkaround = false; // TODO remove this workaround - private boolean skipWorkAround = Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH; - @Override public void onDestroyActionMode(ActionMode mode) { - if (skipWorkAround || actionModeDestroyWorkaround) { - mActionMode = null; - selectedFeed = null; - fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); - actionModeDestroyWorkaround = false; - } else { - actionModeDestroyWorkaround = true; - } + mActionMode = null; + selectedFeed = null; + fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); } @Override diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java deleted file mode 100644 index 845a23823..000000000 --- a/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.gpoddernet; - -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; - -/** - * HTTP client for the gpodder.net service. - */ -public class GpodnetClient extends DefaultHttpClient { - - private static SchemeRegistry prepareSchemeRegistry() { - SchemeRegistry sr = new SchemeRegistry(); - - Scheme http = new Scheme("http", - PlainSocketFactory.getSocketFactory(), 80); - sr.register(http); - Scheme https = new Scheme("https", - SSLSocketFactory.getSocketFactory(), 443); - sr.register(https); - - return sr; - } - - @Override - protected ClientConnectionManager createClientConnectionManager() { - return new ThreadSafeClientConnManager(new BasicHttpParams(), prepareSchemeRegistry()); - } - -} diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java index 6e819f570..a0c5b534c 100644 --- a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java @@ -1,9 +1,8 @@ package de.danoeh.antennapod.gpoddernet; -import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.gpoddernet.model.*; import de.danoeh.antennapod.preferences.GpodnetPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.download.AntennapodHttpClient; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -11,15 +10,13 @@ import org.apache.http.HttpStatus; import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; import org.apache.http.impl.auth.BasicScheme; -import org.apache.http.params.CoreProtocolPNames; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -45,16 +42,10 @@ public class GpodnetService { public static final String DEFAULT_BASE_HOST = "gpodder.net"; private final String BASE_HOST; - private static final int TIMEOUT_MILLIS = 20000; - - private final GpodnetClient httpClient; + private final HttpClient httpClient; public GpodnetService() { - httpClient = new GpodnetClient(); - final HttpParams params = httpClient.getParams(); - params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT); - HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_MILLIS); - HttpConnectionParams.setSoTimeout(params, TIMEOUT_MILLIS); + httpClient = AntennapodHttpClient.getHttpClient(); BASE_HOST = GpodnetPreferences.getHostname(); } @@ -519,7 +510,7 @@ public class GpodnetService { new Thread() { @Override public void run() { - httpClient.getConnectionManager().shutdown(); + AntennapodHttpClient.cleanup(); } }.start(); } diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java index f00d6245c..2b4b66362 100644 --- a/src/de/danoeh/antennapod/preferences/UserPreferences.java +++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java @@ -40,6 +40,7 @@ public class UserPreferences implements public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; public static final String PREF_THEME = "prefTheme"; public static final String PREF_DATA_FOLDER = "prefDataFolder"; public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; @@ -50,6 +51,9 @@ public class UserPreferences implements private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + // TODO: Make this value configurable + private static final double PLAYED_DURATION_AUTOFLATTR_THRESHOLD = 0.8; + private static int EPISODE_CACHE_SIZE_UNLIMITED = -1; private static UserPreferences instance; @@ -63,6 +67,7 @@ public class UserPreferences implements private boolean allowMobileUpdate; private boolean displayOnlyEpisodes; private boolean autoDelete; + private boolean autoFlattr; private int theme; private boolean enableAutodownload; private boolean enableAutodownloadWifiFilter; @@ -112,6 +117,7 @@ public class UserPreferences implements allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); theme = readThemeValue(sp.getString(PREF_THEME, "0")); enableAutodownloadWifiFilter = sp.getBoolean( PREF_ENABLE_AUTODL_WIFI_FILTER, false); @@ -223,6 +229,11 @@ public class UserPreferences implements instanceAvailable(); return instance.autoDelete; } + + public static boolean isAutoFlattr() { + instanceAvailable(); + return instance.autoFlattr; + } public static int getTheme() { instanceAvailable(); @@ -296,6 +307,8 @@ public class UserPreferences implements } else if (key.equals(PREF_AUTO_DELETE)) { autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + } else if (key.equals(PREF_AUTO_FLATTR)) { + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); @@ -319,7 +332,9 @@ public class UserPreferences implements PREF_PLAYBACK_SPEED_ARRAY, null)); } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) { pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - } + } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) { + pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + } } public static void setPlaybackSpeed(String speed) { @@ -506,4 +521,9 @@ public class UserPreferences implements instanceAvailable(); return instance.readEpisodeCacheSizeInternal(valueFromPrefs); } + + public static double getPlayedDurationAutoflattrThreshold() { + instanceAvailable(); + return PLAYED_DURATION_AUTOFLATTR_THRESHOLD; + } } diff --git a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java index c57070091..a53ad486a 100644 --- a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java +++ b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.util.Log; import android.view.KeyEvent; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; /** Receives media button events. */ public class MediaButtonReceiver extends BroadcastReceiver { diff --git a/src/de/danoeh/antennapod/receiver/PlayerWidget.java b/src/de/danoeh/antennapod/receiver/PlayerWidget.java index a3d849972..25bb53475 100644 --- a/src/de/danoeh/antennapod/receiver/PlayerWidget.java +++ b/src/de/danoeh/antennapod/receiver/PlayerWidget.java @@ -6,7 +6,7 @@ import android.content.Context; import android.content.Intent; import android.util.Log; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.service.PlayerWidgetService; +import de.danoeh.antennapod.service.playback.PlayerWidgetService; public class PlayerWidget extends AppWidgetProvider { private static final String TAG = "PlayerWidget"; diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java deleted file mode 100644 index 0bb8753c1..000000000 --- a/src/de/danoeh/antennapod/service/PlaybackService.java +++ /dev/null @@ -1,1734 +0,0 @@ -package de.danoeh.antennapod.service; - -import java.io.IOException; -import java.util.Date; -import java.util.List; -import java.util.concurrent.*; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; -import android.media.AudioManager.OnAudioFocusChangeListener; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.media.RemoteControlClient.MetadataEditor; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.view.KeyEvent; -import android.view.SurfaceHolder; -import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AudioplayerActivity; -import de.danoeh.antennapod.activity.VideoplayerActivity; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.receiver.PlayerWidget; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.BitmapDecoder; -import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.DuckType; -import de.danoeh.antennapod.util.flattr.FlattrUtils; -import de.danoeh.antennapod.util.playback.AudioPlayer; -import de.danoeh.antennapod.util.playback.IPlayer; -import de.danoeh.antennapod.util.playback.Playable; -import de.danoeh.antennapod.util.playback.Playable.PlayableException; -import de.danoeh.antennapod.util.playback.VideoPlayer; -import de.danoeh.antennapod.util.playback.PlaybackController; - -/** - * Controls the MediaPlayer that plays a FeedMedia-file - */ -public class PlaybackService extends Service { - /** - * Logging tag - */ - private static final String TAG = "PlaybackService"; - - /** - * Parcelable of type Playable. - */ - public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** - * True if media should be streamed. - */ - public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream"; - /** - * True if playback should be started immediately after media has been - * prepared. - */ - public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared"; - - public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; - - public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; - private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; - - public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification"; - public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode"; - public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType"; - - /** - * If the PlaybackService receives this action, it will stop playback and - * try to shutdown. - */ - public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; - - /** - * If the PlaybackService receives this action, it will end playback of the - * current episode and load the next episode if there is one available. - */ - public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; - - /** - * Used in NOTIFICATION_TYPE_RELOAD. - */ - public static final int EXTRA_CODE_AUDIO = 1; - public static final int EXTRA_CODE_VIDEO = 2; - - public static final int NOTIFICATION_TYPE_ERROR = 0; - public static final int NOTIFICATION_TYPE_INFO = 1; - public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; - - /** - * Receivers of this intent should update their information about the curently playing media - */ - public static final int NOTIFICATION_TYPE_RELOAD = 3; - /** - * The state of the sleeptimer changed. - */ - public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; - public static final int NOTIFICATION_TYPE_BUFFER_START = 5; - public static final int NOTIFICATION_TYPE_BUFFER_END = 6; - /** - * No more episodes are going to be played. - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; - - /** - * Playback speed has changed - * */ - public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; - - /** - * Returned by getPositionSafe() or getDurationSafe() if the playbackService - * is in an invalid state. - */ - public static final int INVALID_TIME = -1; - - /** - * Is true if service is running. - */ - public static boolean isRunning = false; - - private static final int NOTIFICATION_ID = 1; - - private volatile IPlayer player; - private RemoteControlClient remoteControlClient; - private AudioManager audioManager; - private ComponentName mediaButtonReceiver; - - private volatile Playable media; - - /** - * True if media should be streamed (Extracted from Intent Extra) . - */ - private boolean shouldStream; - - private boolean startWhenPrepared; - private PlayerStatus status; - - private PositionSaver positionSaver; - private ScheduledFuture positionSaverFuture; - - private WidgetUpdateWorker widgetUpdater; - private ScheduledFuture widgetUpdaterFuture; - - private SleepTimer sleepTimer; - private Future sleepTimerFuture; - - private static final int SCHED_EX_POOL_SIZE = 3; - private ScheduledThreadPoolExecutor schedExecutor; - private ExecutorService dbLoaderExecutor; - - private volatile PlayerStatus statusBeforeSeek; - - private static boolean playingVideo; - - /** - * True if mediaplayer was paused because it lost audio focus temporarily - */ - private boolean pausedBecauseOfTransientAudiofocusLoss; - - private Thread chapterLoader; - - private final IBinder mBinder = new LocalBinder(); - - private volatile List<FeedItem> queue; - - public class LocalBinder extends Binder { - public PlaybackService getService() { - return PlaybackService.this; - } - } - - @Override - public boolean onUnbind(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received onUnbind event"); - return super.onUnbind(intent); - } - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - */ - public static Intent getPlayerActivityIntent(Context context) { - if (isRunning) { - if (playingVideo) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } else { - if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - } - - /** - * Same as getPlayerActivityIntent(context), but here the type of activity - * depends on the FeedMedia that is provided as an argument. - */ - public static Intent getPlayerActivityIntent(Context context, Playable media) { - MediaType mt = media.getMediaType(); - if (mt == MediaType.VIDEO) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - super.onCreate(); - if (AppConfig.DEBUG) - Log.d(TAG, "Service created."); - isRunning = true; - pausedBecauseOfTransientAudiofocusLoss = false; - status = PlayerStatus.STOPPED; - audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, "SchedEx rejected submission of new task"); - } - } - ); - dbLoaderExecutor = Executors.newSingleThreadExecutor(); - - mediaButtonReceiver = new ComponentName(getPackageName(), - MediaButtonReceiver.class.getName()); - audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager - .registerRemoteControlClient(setupRemoteControlClient()); - } - registerReceiver(headsetDisconnected, new IntentFilter( - Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - registerReceiver(audioBecomingNoisy, new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( - ACTION_SKIP_CURRENT_EPISODE)); - EventDistributor.getInstance().register(eventDistributorListener); - loadQueue(); - } - - private IPlayer createMediaPlayer() { - IPlayer player; - if (media == null || media.getMediaType() == MediaType.VIDEO) { - player = new VideoPlayer(); - } else { - player = new AudioPlayer(this); - } - return createMediaPlayer(player); - } - - private IPlayer createMediaPlayer(IPlayer mp) { - if (mp != null && media != null) { - if (media.getMediaType() == MediaType.AUDIO) { - ((AudioPlayer) mp).setOnPreparedListener(audioPreparedListener); - ((AudioPlayer) mp) - .setOnCompletionListener(audioCompletionListener); - ((AudioPlayer) mp) - .setOnSeekCompleteListener(audioSeekCompleteListener); - ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); - ((AudioPlayer) mp) - .setOnBufferingUpdateListener(audioBufferingUpdateListener); - ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); - } else { - ((VideoPlayer) mp).setOnPreparedListener(videoPreparedListener); - ((VideoPlayer) mp) - .setOnCompletionListener(videoCompletionListener); - ((VideoPlayer) mp) - .setOnSeekCompleteListener(videoSeekCompleteListener); - ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); - ((VideoPlayer) mp) - .setOnBufferingUpdateListener(videoBufferingUpdateListener); - ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); - } - } - return mp; - } - - @SuppressLint("NewApi") - @Override - public void onDestroy() { - super.onDestroy(); - if (AppConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); - isRunning = false; - if (chapterLoader != null) { - chapterLoader.interrupt(); - } - disableSleepTimer(); - unregisterReceiver(headsetDisconnected); - unregisterReceiver(shutdownReceiver); - unregisterReceiver(audioBecomingNoisy); - unregisterReceiver(skipCurrentEpisodeReceiver); - EventDistributor.getInstance().unregister(eventDistributorListener); - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager.unregisterRemoteControlClient(remoteControlClient); - } - audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); - audioManager.abandonAudioFocus(audioFocusChangeListener); - player.release(); - stopWidgetUpdater(); - updateWidget(); - } - - @Override - public IBinder onBind(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received onBind event"); - return mBinder; - } - - private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { - loadQueue(); - } - } - }; - - private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() { - - @Override - public void onAudioFocusChange(int focusChange) { - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); - pause(true, false); - stopSelf(); - break; - case AudioManager.AUDIOFOCUS_GAIN: - if (AppConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); - if (pausedBecauseOfTransientAudiofocusLoss) { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, 0); - play(); - } - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - if (status == PlayerStatus.PLAYING) { - if (!UserPreferences.shouldPauseForFocusLoss()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, 0); - pausedBecauseOfTransientAudiofocusLoss = true; - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - if (status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - } - }; - - /** - * 1. Check type of intent - * 1.1 Keycode -> handle keycode -> done - * 1.2 Playable -> Step 2 - * 2. Handle playable - * 2.1 Check current status - * 2.1.1 Not playing -> play new playable - * 2.1.2 Playing, new playable is the same -> play if playback is currently paused - * 2.1.3 Playing, new playable different -> Stop playback of old media - * - * @param intent - * @param flags - * @param startId - * @return - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - if (AppConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); - final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - if (keycode == -1 && playable == null) { - Log.e(TAG, "PlaybackService was started with no arguments"); - stopSelf(); - } - - if (keycode != -1) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received media button event"); - handleKeycode(keycode); - } else { - boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - if (media == null) { - media = playable; - shouldStream = playbackType; - startWhenPrepared = intent.getBooleanExtra( - EXTRA_START_WHEN_PREPARED, false); - initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false)); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } - if (media != null) { - if (!playable.getIdentifier().equals(media.getIdentifier())) { - // different media or different playback type - pause(true, false); - player.reset(); - media = playable; - shouldStream = playbackType; - startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); - initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false)); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } else { - // same media and same playback type - if (status == PlayerStatus.PAUSED) { - play(); - } - } - } - } - - return Service.START_NOT_STICKY; - } - - /** Handles media button events */ - private void handleKeycode(int keycode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling keycode: " + keycode); - switch (keycode) { - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (status == PlayerStatus.PLAYING) { - pause(true, true); - } else if (status == PlayerStatus.PAUSED) { - play(); - } else if (status == PlayerStatus.PREPARING) { - setStartWhenPrepared(!startWhenPrepared); - } else if (status == PlayerStatus.INITIALIZED) { - startWhenPrepared = true; - prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - if (status == PlayerStatus.PAUSED) { - play(); - } else if (status == PlayerStatus.INITIALIZED) { - startWhenPrepared = true; - prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - if (status == PlayerStatus.PLAYING) { - pause(true, true); - } - break; - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { - seekDelta(PlaybackController.DEFAULT_SEEK_DELTA); - break; - } - case KeyEvent.KEYCODE_MEDIA_REWIND: { - seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA); - break; - } - } - } - - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - public void setVideoSurface(SurfaceHolder sh) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting display"); - player.setDisplay(null); - player.setDisplay(sh); - if (status == PlayerStatus.STOPPED - || status == PlayerStatus.AWAITING_VIDEO_SURFACE) { - try { - InitTask initTask = new InitTask() { - - @Override - protected void onPostExecute(Playable result) { - if (status == PlayerStatus.INITIALIZING) { - if (result != null) { - try { - if (shouldStream) { - player.setDataSource(media - .getStreamUrl()); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } else { - player.setDataSource(media - .getLocalMediaUrl()); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } else { - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } - } - - @Override - protected void onPreExecute() { - setStatus(PlayerStatus.INITIALIZING); - } - - }; - initTask.executeAsync(media); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - - } - - /** - * Called when the surface holder of the mediaplayer has to be changed. - */ - private void resetVideoSurface() { - if (AppConfig.DEBUG) - Log.d(TAG, "Resetting video surface"); - cancelPositionSaver(); - player.setDisplay(null); - player.reset(); - player.release(); - player = createMediaPlayer(); - status = PlayerStatus.STOPPED; - } - - public void notifyVideoSurfaceAbandoned() { - resetVideoSurface(); - if (media != null) { - initMediaplayer(true); - } - } - - /** - * Called after service has extracted the media it is supposed to play. - * - * @param prepareImmediately True if service should prepare playback after it has been initialized - */ - private void initMediaplayer(final boolean prepareImmediately) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting up media player"); - try { - MediaType mediaType = media.getMediaType(); - player = createMediaPlayer(); - if (mediaType == MediaType.AUDIO) { - if (AppConfig.DEBUG) - Log.d(TAG, "Mime type is audio"); - - InitTask initTask = new InitTask() { - - @Override - protected void onPostExecute(Playable result) { - // check if state of service has changed. If it has - // changed, assume that loaded metadata is not needed - // anymore. - if (status == PlayerStatus.INITIALIZING) { - if (result != null) { - playingVideo = false; - try { - if (shouldStream) { - player.setDataSource(media - .getStreamUrl()); - } else if (media.localFileAvailable()) { - player.setDataSource(media - .getLocalMediaUrl()); - } - - if (prepareImmediately) { - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } else { - setStatus(PlayerStatus.INITIALIZED); - } - } catch (IOException e) { - e.printStackTrace(); - media = null; - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } else { - Log.e(TAG, "InitTask could not load metadata"); - media = null; - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "Status of player has changed during initialization. Stopping init process."); - } - } - - @Override - protected void onPreExecute() { - setStatus(PlayerStatus.INITIALIZING); - } - - }; - initTask.executeAsync(media); - } else if (mediaType == MediaType.VIDEO) { - if (AppConfig.DEBUG) - Log.d(TAG, "Mime type is video"); - playingVideo = true; - setStatus(PlayerStatus.AWAITING_VIDEO_SURFACE); - player.setScreenOnWhilePlaying(true); - } - - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - - private void setupPositionSaver() { - if (positionSaverFuture == null - || (positionSaverFuture.isCancelled() || positionSaverFuture - .isDone())) { - - positionSaver = new PositionSaver(); - positionSaverFuture = schedExecutor.scheduleAtFixedRate( - positionSaver, PositionSaver.WAITING_INTERVALL, - PositionSaver.WAITING_INTERVALL, TimeUnit.MILLISECONDS); - } - } - - private void cancelPositionSaver() { - if (positionSaverFuture != null) { - boolean result = positionSaverFuture.cancel(true); - if (AppConfig.DEBUG) - Log.d(TAG, "PositionSaver cancelled. Result: " + result); - } - } - - private final com.aocate.media.MediaPlayer.OnPreparedListener audioPreparedListener = new com.aocate.media.MediaPlayer.OnPreparedListener() { - @Override - public void onPrepared(com.aocate.media.MediaPlayer mp) { - genericOnPrepared(mp); - } - }; - - private final android.media.MediaPlayer.OnPreparedListener videoPreparedListener = new android.media.MediaPlayer.OnPreparedListener() { - @Override - public void onPrepared(android.media.MediaPlayer mp) { - genericOnPrepared(mp); - } - }; - - private final void genericOnPrepared(Object inObj) { - IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class); - if (AppConfig.DEBUG) - Log.d(TAG, "Resource prepared"); - mp.seekTo(media.getPosition()); - if (media.getDuration() == 0) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting duration of media"); - media.setDuration(mp.getDuration()); - } - setStatus(PlayerStatus.PREPARED); - if (chapterLoader != null) { - chapterLoader.interrupt(); - } - chapterLoader = new Thread() { - @Override - public void run() { - if (AppConfig.DEBUG) - Log.d(TAG, "Chapter loader started"); - if (media != null && media.getChapters() == null) { - media.loadChapterMarks(); - if (!isInterrupted() && media.getChapters() != null) { - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - 0); - } - } - if (AppConfig.DEBUG) - Log.d(TAG, "Chapter loader stopped"); - } - }; - chapterLoader.start(); - - if (startWhenPrepared) { - play(); - } - } - - private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(com.aocate.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(android.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final void genericSeekCompleteListener() { - if (status == PlayerStatus.SEEKING) { - setStatus(statusBeforeSeek); - } - } - - private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericInfoListener(what); - } - }; - - private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { - return genericInfoListener(what); - } - }; - - private boolean genericInfoListener(int what) { - switch (what) { - case MediaPlayer.MEDIA_INFO_BUFFERING_START: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); - return true; - case MediaPlayer.MEDIA_INFO_BUFFERING_END: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); - return true; - default: - return false; - } - } - - private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericOnError(mp, what, extra); - } - }; - - private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(android.media.MediaPlayer mp, int what, int extra) { - return genericOnError(mp, what, extra); - } - }; - - private boolean genericOnError(Object inObj, int what, int extra) { - final String TAG = "PlaybackService.onErrorListener"; - Log.w(TAG, "An error has occured: " + what + " " + extra); - IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class); - if (mp.isPlaying()) { - pause(true, true); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); - setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - stopSelf(); - return true; - } - - private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(com.aocate.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(android.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private void genericOnCompletion() { - endPlayback(true); - } - - private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, - int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private void genericOnBufferingUpdate(int percent) { - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); - } - - private void endPlayback(boolean playNextEpisode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Playback ended"); - audioManager.abandonAudioFocus(audioFocusChangeListener); - - // Save state - cancelPositionSaver(); - - boolean isInQueue = false; - FeedItem nextItem = null; - - if (media instanceof FeedMedia) { - FeedItem item = ((FeedMedia) media).getItem(); - DBWriter.markItemRead(PlaybackService.this, item, true, true); - nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); - isInQueue = media instanceof FeedMedia - && QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); - } - DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); - long autoDeleteMediaId = ((FeedComponent) media).getId(); - if (shouldStream) { - autoDeleteMediaId = -1; - } - } - - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - boolean loadNextItem = isInQueue && nextItem != null; - playNextEpisode = playNextEpisode && loadNextItem - && UserPreferences.isFollowQueue(); - if (loadNextItem) { - if (AppConfig.DEBUG) - Log.d(TAG, "Loading next item in queue"); - media = nextItem.getMedia(); - } - final boolean prepareImmediately; - if (playNextEpisode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "No more episodes available to play"); - media = null; - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - int notificationCode = 0; - if (media != null) { - shouldStream = !media.localFileAvailable(); - if (media.getMediaType() == MediaType.AUDIO) { - notificationCode = EXTRA_CODE_AUDIO; - playingVideo = false; - } else if (media.getMediaType() == MediaType.VIDEO) { - notificationCode = EXTRA_CODE_VIDEO; - } - } - writePlaybackPreferences(); - if (media != null) { - resetVideoSurface(); - refreshRemoteControlClientState(); - initMediaplayer(prepareImmediately); - - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - notificationCode); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - stopSelf(); - } - } - - public void setSleepTimer(long waitingTime) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); - if (sleepTimerFuture != null) { - sleepTimerFuture.cancel(true); - } - sleepTimer = new SleepTimer(waitingTime); - sleepTimerFuture = schedExecutor.submit(sleepTimer); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - public void disableSleepTimer() { - if (sleepTimerFuture != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Disabling sleep timer"); - sleepTimerFuture.cancel(true); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - } - - /** - * Saves the current position and pauses playback. Note that, if audiofocus - * is abandoned, the lockscreen controls will also disapear. - * - * @param abandonFocus - * is true if the service should release audio focus - * @param reinit - * is true if service should reinit after pausing if the media - * file is being streamed - */ - public void pause(boolean abandonFocus, boolean reinit) { - if (player.isPlaying()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Pausing playback."); - player.pause(); - cancelPositionSaver(); - saveCurrentPosition(); - setStatus(PlayerStatus.PAUSED); - if (abandonFocus) { - audioManager.abandonAudioFocus(audioFocusChangeListener); - pausedBecauseOfTransientAudiofocusLoss = false; - disableSleepTimer(); - } - stopWidgetUpdater(); - stopForeground(true); - if (shouldStream && reinit) { - reinit(); - } - } - } - - /** Pauses playback and destroys service. Recommended for video playback. */ - public void stop() { - if (AppConfig.DEBUG) - Log.d(TAG, "Stopping playback"); - if (status == PlayerStatus.PREPARED || status == PlayerStatus.PAUSED - || status == PlayerStatus.STOPPED - || status == PlayerStatus.PLAYING) { - player.stop(); - } - setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - stopSelf(); - } - - /** - * Prepared media player for playback if the service is in the INITALIZED - * state. - */ - public void prepare() { - if (status == PlayerStatus.INITIALIZED) { - if (AppConfig.DEBUG) - Log.d(TAG, "Preparing media player"); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } - } - - /** Resets the media player and moves into INITIALIZED state. */ - public void reinit() { - player.reset(); - player = createMediaPlayer(player); - initMediaplayer(false); - } - - @SuppressLint("NewApi") - public void play() { - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED - || status == PlayerStatus.STOPPED) { - int focusGained = audioManager.requestAudioFocus( - audioFocusChangeListener, AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - - if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - if (AppConfig.DEBUG) - Log.d(TAG, "Audiofocus successfully requested"); - if (AppConfig.DEBUG) - Log.d(TAG, "Resuming/Starting playback"); - writePlaybackPreferences(); - - setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); - player.start(); - if (status != PlayerStatus.PAUSED) { - player.seekTo((int) media.getPosition()); - } - setStatus(PlayerStatus.PLAYING); - setupPositionSaver(); - setupWidgetUpdater(); - setupNotification(); - pausedBecauseOfTransientAudiofocusLoss = false; - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager - .registerRemoteControlClient(remoteControlClient); - } - audioManager - .registerMediaButtonEventReceiver(mediaButtonReceiver); - media.onPlaybackStart(); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Failed to request Audiofocus"); - } - } - } - - private void writePlaybackPreferences() { - if (AppConfig.DEBUG) - Log.d(TAG, "Writing playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - if (media != null) { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - media.getPlayableType()); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - shouldStream); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, - playingVideo); - if (media instanceof FeedMedia) { - FeedMedia fMedia = (FeedMedia) media; - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - fMedia.getItem().getFeed().getId()); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - fMedia.getId()); - } else { - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - media.writeToPreferences(editor); - } else { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - - editor.commit(); - } - - private void setStatus(PlayerStatus newStatus) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting status to " + newStatus); - status = newStatus; - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - updateWidget(); - refreshRemoteControlClientState(); - bluetoothNotifyChange(); - } - - /** Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. */ - private void postStatusUpdateIntent() { - setStatus(status); - } - - private void sendNotificationBroadcast(int type, int code) { - Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); - intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); - intent.putExtra(EXTRA_NOTIFICATION_CODE, code); - sendBroadcast(intent); - } - - /** Used by setupNotification to load notification data in another thread. */ - private AsyncTask<Void, Void, Void> notificationSetupTask; - - /** Prepares notification and starts the service in the foreground. */ - @SuppressLint("NewApi") - private void setupNotification() { - final PendingIntent pIntent = PendingIntent.getActivity(this, 0, - PlaybackService.getPlayerActivityIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); - - if (notificationSetupTask != null) { - notificationSetupTask.cancel(true); - } - notificationSetupTask = new AsyncTask<Void, Void, Void>() { - Bitmap icon = null; - - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) - Log.d(TAG, "Starting background work"); - if (android.os.Build.VERSION.SDK_INT >= 11) { - if (media != null && media != null) { - int iconSize = getResources().getDimensionPixelSize( - android.R.dimen.notification_large_icon_width); - icon = BitmapDecoder - .decodeBitmapFromWorkerTaskResource(iconSize, - media); - } - - } - if (icon == null) { - icon = BitmapFactory.decodeResource(getResources(), - R.drawable.ic_stat_antenna); - } - - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (!isCancelled() && status == PlayerStatus.PLAYING - && media != null) { - String contentText = media.getFeedTitle(); - String contentTitle = media.getEpisodeTitle(); - Notification notification = null; - if (android.os.Build.VERSION.SDK_INT >= 16) { - Intent pauseButtonIntent = new Intent( - PlaybackService.this, PlaybackService.class); - pauseButtonIntent.putExtra( - MediaButtonReceiver.EXTRA_KEYCODE, - KeyEvent.KEYCODE_MEDIA_PAUSE); - PendingIntent pauseButtonPendingIntent = PendingIntent - .getService(PlaybackService.this, 0, - pauseButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - Notification.Builder notificationBuilder = new Notification.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setOngoing(true) - .setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna) - .addAction(android.R.drawable.ic_media_pause, - getString(R.string.pause_label), - pauseButtonPendingIntent); - notification = notificationBuilder.build(); - } else { - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna); - notification = notificationBuilder.getNotification(); - } - startForeground(NOTIFICATION_ID, notification); - if (AppConfig.DEBUG) - Log.d(TAG, "Notification set up"); - } - } - - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - notificationSetupTask - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - notificationSetupTask.execute(); - } - - } - - /** - * Seek a specific position from the current position - * - * @param delta - * offset from current position (positive or negative) - * */ - public void seekDelta(int delta) { - int position = getCurrentPositionSafe(); - if (position != INVALID_TIME) { - seek(player.getCurrentPosition() + delta); - } - } - - public void seek(int i) { - saveCurrentPosition(); - if (status == PlayerStatus.INITIALIZED - || status == PlayerStatus.INITIALIZING - || status == PlayerStatus.PREPARING) { - media.setPosition(i); - setStartWhenPrepared(true); - prepare(); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Seeking position " + i); - if (shouldStream) { - if (status != PlayerStatus.SEEKING) { - statusBeforeSeek = status; - } - setStatus(PlayerStatus.SEEKING); - } - player.seekTo(i); - } - } - - public void seekToChapter(Chapter chapter) { - seek((int) chapter.getStart()); - } - - /** Saves the current position of the media file to the DB */ - private synchronized void saveCurrentPosition() { - int position = getCurrentPositionSafe(); - if (position != INVALID_TIME) { - if (AppConfig.DEBUG) - Log.d(TAG, "Saving current position to " + position); - media.saveCurrentPosition(PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()), - position); - } - } - - private void stopWidgetUpdater() { - if (widgetUpdaterFuture != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Stopping widgetUpdateWorker"); - widgetUpdaterFuture.cancel(true); - } - sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE)); - } - - @SuppressLint("NewApi") - private void setupWidgetUpdater() { - if (widgetUpdaterFuture == null - || (widgetUpdaterFuture.isCancelled() || widgetUpdaterFuture - .isDone())) { - widgetUpdater = new WidgetUpdateWorker(); - widgetUpdaterFuture = schedExecutor.scheduleAtFixedRate( - widgetUpdater, WidgetUpdateWorker.NOTIFICATION_INTERVALL, - WidgetUpdateWorker.NOTIFICATION_INTERVALL, - TimeUnit.MILLISECONDS); - } - } - - private void updateWidget() { - if (AppConfig.DEBUG) - Log.d(TAG, "Sending widget update request"); - PlaybackService.this.sendBroadcast(new Intent( - PlayerWidget.FORCE_WIDGET_UPDATE)); - } - - public boolean sleepTimerActive() { - return sleepTimer != null && sleepTimer.isWaiting(); - } - - public long getSleepTimerTimeLeft() { - if (sleepTimerActive()) { - return sleepTimer.getWaitingTime(); - } else { - return 0; - } - } - - @SuppressLint("NewApi") - private RemoteControlClient setupRemoteControlClient() { - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(mediaButtonReceiver); - PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( - getApplicationContext(), 0, mediaButtonIntent, 0); - remoteControlClient = new RemoteControlClient(mediaPendingIntent); - int controlFlags; - if (android.os.Build.VERSION.SDK_INT < 16) { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE - | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; - } else { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; - } - remoteControlClient.setTransportControlFlags(controlFlags); - return remoteControlClient; - } - - /** Refresh player status and metadata. */ - @SuppressLint("NewApi") - private void refreshRemoteControlClientState() { - if (android.os.Build.VERSION.SDK_INT >= 14) { - if (remoteControlClient != null) { - switch (status) { - case PLAYING: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - break; - case PAUSED: - case INITIALIZED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - break; - case STOPPED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - break; - case ERROR: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); - break; - default: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); - } - if (media != null) { - MetadataEditor editor = remoteControlClient - .editMetadata(false); - editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, - media.getEpisodeTitle()); - - editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, - media.getFeedTitle()); - - editor.apply(); - } - if (AppConfig.DEBUG) - Log.d(TAG, "RemoteControlClient state was refreshed"); - } - } - } - - private void bluetoothNotifyChange() { - boolean isPlaying = false; - - if (status == PlayerStatus.PLAYING) { - isPlaying = true; - } - - if (media != null) { - Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED); - i.putExtra("id", 1); - i.putExtra("artist", ""); - i.putExtra("album", media.getFeedTitle()); - i.putExtra("track", media.getEpisodeTitle()); - i.putExtra("playing", isPlaying); - if (queue != null) { - i.putExtra("ListSize", queue.size()); - } - i.putExtra("duration", media.getDuration()); - i.putExtra("position", media.getPosition()); - sendBroadcast(i); - } - } - - /** - * Pauses playback when the headset is disconnected and the preference is - * set - */ - private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { - private static final String TAG = "headsetDisconnected"; - private static final int UNPLUGGED = 0; - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) { - int state = intent.getIntExtra("state", -1); - if (state != -1) { - if (AppConfig.DEBUG) - Log.d(TAG, "Headset plug event. State is " + state); - if (state == UNPLUGGED && status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - Log.d(TAG, "Headset was unplugged during playback."); - pauseIfPauseOnDisconnect(); - } - } else { - Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); - } - } - } - }; - - private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - // sound is about to change, eg. bluetooth -> speaker - if (AppConfig.DEBUG) - Log.d(TAG, "Pausing playback because audio is becoming noisy"); - pauseIfPauseOnDisconnect(); - } - // android.media.AUDIO_BECOMING_NOISY - }; - - /** Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. */ - private void pauseIfPauseOnDisconnect() { - if (UserPreferences.isPauseOnHeadsetDisconnect() - && status == PlayerStatus.PLAYING) { - pause(true, true); - } - } - - private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - schedExecutor.shutdownNow(); - stop(); - media = null; - } - } - - }; - - private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) { - - if (AppConfig.DEBUG) - Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - if (media != null) { - setStatus(PlayerStatus.STOPPED); - endPlayback(true); - } - } - } - }; - - /** Periodically saves the position of the media file */ - class PositionSaver implements Runnable { - public static final int WAITING_INTERVALL = 5000; - - @Override - public void run() { - if (player != null && player.isPlaying()) { - try { - saveCurrentPosition(); - } catch (IllegalStateException e) { - Log.w(TAG, - "saveCurrentPosition was called in illegal state"); - } - } - } - } - - /** Notifies the player widget in the specified intervall */ - class WidgetUpdateWorker implements Runnable { - private static final int NOTIFICATION_INTERVALL = 1000; - - @Override - public void run() { - if (PlaybackService.isRunning) { - updateWidget(); - } - } - } - - /** Sleeps for a given time and then pauses playback. */ - class SleepTimer implements Runnable { - private static final String TAG = "SleepTimer"; - private static final long UPDATE_INTERVALL = 1000L; - private volatile long waitingTime; - private boolean isWaiting; - - public SleepTimer(long waitingTime) { - super(); - this.waitingTime = waitingTime; - } - - @Override - public void run() { - isWaiting = true; - if (AppConfig.DEBUG) - Log.d(TAG, "Starting"); - while (waitingTime > 0) { - try { - Thread.sleep(UPDATE_INTERVALL); - waitingTime -= UPDATE_INTERVALL; - - if (waitingTime <= 0) { - if (AppConfig.DEBUG) - Log.d(TAG, "Waiting completed"); - if (status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - Log.d(TAG, "Pausing playback"); - pause(true, true); - } - postExecute(); - } - } catch (InterruptedException e) { - Log.d(TAG, "Thread was interrupted while waiting"); - break; - } - } - postExecute(); - } - - protected void postExecute() { - isWaiting = false; - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - public long getWaitingTime() { - return waitingTime; - } - - public boolean isWaiting() { - return isWaiting; - } - - } - - public static boolean isPlayingVideo() { - return playingVideo; - } - - public boolean isShouldStream() { - return shouldStream; - } - - public PlayerStatus getStatus() { - return status; - } - - public Playable getMedia() { - return media; - } - - public IPlayer getPlayer() { - return player; - } - - public boolean isStartWhenPrepared() { - return startWhenPrepared; - } - - public void setStartWhenPrepared(boolean startWhenPrepared) { - this.startWhenPrepared = startWhenPrepared; - postStatusUpdateIntent(); - } - - public boolean canSetSpeed() { - if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) { - return ((AudioPlayer) player).canSetSpeed(); - } - return false; - } - - public boolean canSetPitch() { - if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) { - return ((AudioPlayer) player).canSetPitch(); - } - return false; - } - - public void setSpeed(float speed) { - if (media != null && media.getMediaType() == MediaType.AUDIO) { - AudioPlayer audioPlayer = (AudioPlayer) player; - if (audioPlayer.canSetSpeed()) { - audioPlayer.setPlaybackSpeed((float) speed); - if (AppConfig.DEBUG) - Log.d(TAG, "Playback speed was set to " + speed); - sendNotificationBroadcast( - NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); - } - } - } - - public void setPitch(float pitch) { - if (media != null && media.getMediaType() == MediaType.AUDIO) { - AudioPlayer audioPlayer = (AudioPlayer) player; - if (audioPlayer.canSetPitch()) { - audioPlayer.setPlaybackPitch((float) pitch); - } - } - } - - public float getCurrentPlaybackSpeed() { - if (media.getMediaType() == MediaType.AUDIO - && player instanceof AudioPlayer) { - AudioPlayer audioPlayer = (AudioPlayer) player; - if (audioPlayer.canSetSpeed()) { - return audioPlayer.getCurrentSpeedMultiplier(); - } - } - return -1; - } - - /** - * call getDuration() on mediaplayer or return INVALID_TIME if player is in - * an invalid state. This method should be used instead of calling - * getDuration() directly to avoid an error. - */ - public int getDurationSafe() { - if (status != null && player != null) { - switch (status) { - case PREPARED: - case PLAYING: - case PAUSED: - case SEEKING: - try { - return player.getDuration(); - } catch (IllegalStateException e) { - e.printStackTrace(); - return INVALID_TIME; - } - default: - return INVALID_TIME; - } - } else { - return INVALID_TIME; - } - } - - /** - * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player - * is in an invalid state. This method should be used instead of calling - * getCurrentPosition() directly to avoid an error. - */ - public int getCurrentPositionSafe() { - if (status != null && player != null) { - switch (status) { - case PREPARED: - case PLAYING: - case PAUSED: - case SEEKING: - return player.getCurrentPosition(); - default: - return INVALID_TIME; - } - } else { - return INVALID_TIME; - } - } - - private void setCurrentlyPlayingMedia(long id) { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id); - editor.commit(); - } - - private static class InitTask extends AsyncTask<Playable, Void, Playable> { - private Playable playable; - public PlayableException exception; - - @Override - protected Playable doInBackground(Playable... params) { - if (params[0] == null) { - throw new IllegalArgumentException("Playable must not be null"); - } - playable = params[0]; - - try { - playable.loadMetadata(); - } catch (PlayableException e) { - e.printStackTrace(); - exception = e; - return null; - } - return playable; - } - - @SuppressLint("NewApi") - public void executeAsync(Playable playable) { - FlattrUtils.hasToken(); - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - executeOnExecutor(THREAD_POOL_EXECUTOR, playable); - } else { - execute(playable); - } - } - - } - - private void loadQueue() { - dbLoaderExecutor.submit(new QueueLoaderTask()); - } - - private class QueueLoaderTask implements Runnable { - @Override - public void run() { - List<FeedItem> queueRef = DBReader.getQueue(PlaybackService.this); - queue = queueRef; - } - } -} diff --git a/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java new file mode 100644 index 000000000..7e1c9178a --- /dev/null +++ b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java @@ -0,0 +1,95 @@ +package de.danoeh.antennapod.service.download; + +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import org.apache.http.client.HttpClient; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.params.ConnManagerPNames; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.CoreProtocolPNames; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import java.util.concurrent.TimeUnit; + +/** + * Provides access to a HttpClient singleton. + */ +public class AntennapodHttpClient { + private static final String TAG = "AntennapodHttpClient"; + + public static final long EXPIRED_CONN_TIMEOUT_SEC = 30; + + public static final int MAX_REDIRECTS = 5; + public static final int CONNECTION_TIMEOUT = 30000; + public static final int SOCKET_TIMEOUT = 30000; + + public static final int MAX_CONNECTIONS = 6; + + + private static volatile HttpClient httpClient = null; + + /** + * Returns the HttpClient singleton. + */ + public static synchronized HttpClient getHttpClient() { + if (httpClient == null) { + if (AppConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client"); + + HttpParams params = new BasicHttpParams(); + params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT); + params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS); + params.setBooleanParameter("http.protocol.reject-relative-redirect", + false); + HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); + HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); + HttpClientParams.setRedirecting(params, true); + + httpClient = new DefaultHttpClient(createClientConnectionManager(), params); + // Workaround for broken URLs in redirection + ((AbstractHttpClient) httpClient) + .setRedirectHandler(new APRedirectHandler()); + } + return httpClient; + } + + /** + * Closes expired connections. This method should be called by the using class once has finished its work with + * the HTTP client. + */ + public static synchronized void cleanup() { + if (httpClient != null) { + httpClient.getConnectionManager().closeExpiredConnections(); + httpClient.getConnectionManager().closeIdleConnections(EXPIRED_CONN_TIMEOUT_SEC, TimeUnit.SECONDS); + } + } + + + private static ClientConnectionManager createClientConnectionManager() { + HttpParams params = new BasicHttpParams(); + params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, MAX_CONNECTIONS); + return new ThreadSafeClientConnManager(params, prepareSchemeRegistry()); + } + + private static SchemeRegistry prepareSchemeRegistry() { + SchemeRegistry sr = new SchemeRegistry(); + + Scheme http = new Scheme("http", + PlainSocketFactory.getSocketFactory(), 80); + sr.register(http); + Scheme https = new Scheme("https", + SSLSocketFactory.getSocketFactory(), 443); + sr.register(https); + + return sr; + } + +} diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index 4d521b4df..c27b4d4fe 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -11,6 +11,7 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.ParserConfigurationException; +import android.media.MediaMetadataRetriever; import de.danoeh.antennapod.storage.*; import org.xml.sax.SAXException; @@ -25,7 +26,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.media.MediaPlayer; import android.os.AsyncTask; import android.os.Binder; import android.os.Handler; @@ -801,23 +801,21 @@ public class DownloadService extends Service { media.setFile_url(request.getDestination()); // Get duration - MediaPlayer mediaplayer = null; + MediaMetadataRetriever mmr = null; try { - mediaplayer = new MediaPlayer(); - mediaplayer.setDataSource(media.getFile_url()); - mediaplayer.prepare(); - media.setDuration(mediaplayer.getDuration()); + mmr = new MediaMetadataRetriever(); + mmr.setDataSource(media.getFile_url()); + String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + media.setDuration(Integer.parseInt(durationStr)); if (AppConfig.DEBUG) Log.d(TAG, "Duration of file is " + media.getDuration()); - mediaplayer.reset(); - } catch (IOException e) { + } catch (NumberFormatException e) { e.printStackTrace(); } catch (RuntimeException e) { - // Thrown by MediaPlayer initialization on some devices e.printStackTrace(); } finally { - if (mediaplayer != null) { - mediaplayer.release(); + if (mmr != null) { + mmr.release(); } } diff --git a/src/de/danoeh/antennapod/service/download/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java index 84731fe9f..80cc5b3f8 100644 --- a/src/de/danoeh/antennapod/service/download/Downloader.java +++ b/src/de/danoeh/antennapod/service/download/Downloader.java @@ -1,5 +1,8 @@ package de.danoeh.antennapod.service.download; +import android.content.Context; +import android.net.wifi.WifiManager; +import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import java.util.concurrent.Callable; @@ -26,7 +29,19 @@ public abstract class Downloader implements Callable<Downloader> { protected abstract void download(); public final Downloader call() { + WifiManager wifiManager = (WifiManager) PodcastApp.getInstance().getSystemService(Context.WIFI_SERVICE); + WifiManager.WifiLock wifiLock = null; + if (wifiManager != null) { + wifiLock = wifiManager.createWifiLock(TAG); + wifiLock.acquire(); + } + download(); + + if (wifiLock != null) { + wifiLock.release(); + } + if (result == null) { throw new IllegalStateException( "Downloader hasn't created DownloadStatus object"); diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java index 582fb9575..fc2b3178b 100644 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java @@ -1,26 +1,5 @@ package de.danoeh.antennapod.service.download; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; - -import org.apache.commons.io.IOUtils; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.impl.client.AbstractHttpClient; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; - import android.net.http.AndroidHttpClient; import android.util.Log; import de.danoeh.antennapod.AppConfig; @@ -28,44 +7,43 @@ import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.StorageUtils; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; + +import java.io.*; +import java.net.*; public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; - private static final int MAX_REDIRECTS = 5; - private static final int BUFFER_SIZE = 8 * 1024; - private static final int CONNECTION_TIMEOUT = 30000; - private static final int SOCKET_TIMEOUT = 30000; public HttpDownloader(DownloadRequest request) { super(request); } - private DefaultHttpClient createHttpClient() { - DefaultHttpClient httpClient = new DefaultHttpClient(); - HttpParams params = httpClient.getParams(); - params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS); - params.setBooleanParameter("http.protocol.reject-relative-redirect", - false); - HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); - HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); - HttpClientParams.setRedirecting(params, true); - - // Workaround for broken URLs in redirection - ((AbstractHttpClient) httpClient) - .setRedirectHandler(new APRedirectHandler()); - return httpClient; + private URI getURIFromRequestUrl(String source) { + try { + URL url = new URL(source); + return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } } @Override protected void download() { - DefaultHttpClient httpClient = null; + HttpClient httpClient = AntennapodHttpClient.getHttpClient(); BufferedOutputStream out = null; InputStream connection = null; try { - HttpGet httpGet = new HttpGet(request.getSource()); - httpClient = createHttpClient(); + HttpGet httpGet = new HttpGet(getURIFromRequestUrl(request.getSource())); HttpResponse response = httpClient.execute(httpGet); HttpEntity httpEntity = response.getEntity(); int responseCode = response.getStatusLine().getStatusCode(); @@ -167,9 +145,7 @@ public class HttpDownloader extends Downloader { onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); } finally { IOUtils.closeQuietly(out); - if (httpClient != null) { - httpClient.getConnectionManager().shutdown(); - } + AntennapodHttpClient.cleanup(); } } diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java new file mode 100644 index 000000000..6bc8c4127 --- /dev/null +++ b/src/de/danoeh/antennapod/service/playback/PlaybackService.java @@ -0,0 +1,1037 @@ +package de.danoeh.antennapod.service.playback; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.*; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.media.RemoteControlClient.MetadataEditor; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.AudioplayerActivity; +import de.danoeh.antennapod.activity.VideoplayerActivity; +import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.feed.MediaType; +import de.danoeh.antennapod.preferences.PlaybackPreferences; +import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.receiver.PlayerWidget; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.util.BitmapDecoder; +import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.flattr.FlattrUtils; +import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.util.playback.PlaybackController; + +import java.util.List; + +/** + * Controls the MediaPlayer that plays a FeedMedia-file + */ +public class PlaybackService extends Service { + /** + * Logging tag + */ + private static final String TAG = "PlaybackService"; + + /** + * Parcelable of type Playable. + */ + public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; + /** + * True if media should be streamed. + */ + public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream"; + /** + * True if playback should be started immediately after media has been + * prepared. + */ + public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared"; + + public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; + + public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; + private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; + private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; + + public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification"; + public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode"; + public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType"; + + /** + * If the PlaybackService receives this action, it will stop playback and + * try to shutdown. + */ + public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; + + /** + * If the PlaybackService receives this action, it will end playback of the + * current episode and load the next episode if there is one available. + */ + public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; + + /** + * Used in NOTIFICATION_TYPE_RELOAD. + */ + public static final int EXTRA_CODE_AUDIO = 1; + public static final int EXTRA_CODE_VIDEO = 2; + + public static final int NOTIFICATION_TYPE_ERROR = 0; + public static final int NOTIFICATION_TYPE_INFO = 1; + public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; + + /** + * Receivers of this intent should update their information about the curently playing media + */ + public static final int NOTIFICATION_TYPE_RELOAD = 3; + /** + * The state of the sleeptimer changed. + */ + public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; + public static final int NOTIFICATION_TYPE_BUFFER_START = 5; + public static final int NOTIFICATION_TYPE_BUFFER_END = 6; + /** + * No more episodes are going to be played. + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; + + /** + * Playback speed has changed + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; + + /** + * Returned by getPositionSafe() or getDurationSafe() if the playbackService + * is in an invalid state. + */ + public static final int INVALID_TIME = -1; + + /** + * Is true if service is running. + */ + public static boolean isRunning = false; + /** + * Is true if service has received a valid start command. + */ + public static boolean started = false; + + private static final int NOTIFICATION_ID = 1; + + private RemoteControlClient remoteControlClient; + private PlaybackServiceMediaPlayer mediaPlayer; + private PlaybackServiceTaskManager taskManager; + + private static volatile MediaType currentMediaType = MediaType.UNKNOWN; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public PlaybackService getService() { + return PlaybackService.this; + } + } + + @Override + public boolean onUnbind(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received onUnbind event"); + return super.onUnbind(intent); + } + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. If the playbackservice is not + * running, the type of the last played media will be looked up. + */ + public static Intent getPlayerActivityIntent(Context context) { + if (isRunning) { + if (currentMediaType == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } else { + if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + } + + /** + * Same as getPlayerActivityIntent(context), but here the type of activity + * depends on the FeedMedia that is provided as an argument. + */ + public static Intent getPlayerActivityIntent(Context context, Playable media) { + MediaType mt = media.getMediaType(); + if (mt == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + super.onCreate(); + if (AppConfig.DEBUG) + Log.d(TAG, "Service created."); + isRunning = true; + + registerReceiver(headsetDisconnected, new IntentFilter( + Intent.ACTION_HEADSET_PLUG)); + registerReceiver(shutdownReceiver, new IntentFilter( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + registerReceiver(audioBecomingNoisy, new IntentFilter( + AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( + ACTION_SKIP_CURRENT_EPISODE)); + remoteControlClient = setupRemoteControlClient(); + taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); + mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); + + } + + @SuppressLint("NewApi") + @Override + public void onDestroy() { + super.onDestroy(); + if (AppConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + isRunning = false; + started = false; + currentMediaType = MediaType.UNKNOWN; + + unregisterReceiver(headsetDisconnected); + unregisterReceiver(shutdownReceiver); + unregisterReceiver(audioBecomingNoisy); + unregisterReceiver(skipCurrentEpisodeReceiver); + mediaPlayer.shutdown(); + taskManager.shutdown(); + } + + @Override + public IBinder onBind(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received onBind event"); + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + if (AppConfig.DEBUG) + Log.d(TAG, "OnStartCommand called"); + final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); + if (keycode == -1 && playable == null) { + Log.e(TAG, "PlaybackService was started with no arguments"); + stopSelf(); + } + + if ((flags & Service.START_FLAG_REDELIVERY) != 0) { + if (AppConfig.DEBUG) Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + stopForeground(true); + } else { + + if (keycode != -1) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received media button event"); + handleKeycode(keycode); + } else { + started = true; + boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, + true); + boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); + boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); + } + } + + return Service.START_REDELIVER_INTENT; + } + + /** + * Handles media button events + */ + private void handleKeycode(int keycode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling keycode: " + keycode); + + final PlayerStatus status = mediaPlayer.getPSMPInfo().playerStatus; + switch (keycode) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if (status == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, true); + } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.PREPARING) { + mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + if (status == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, true); + } + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + mediaPlayer.seekDelta(PlaybackController.DEFAULT_SEEK_DELTA); + break; + } + case KeyEvent.KEYCODE_MEDIA_REWIND: { + mediaPlayer.seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA); + break; + } + } + } + + /** + * Called by a mediaplayer Activity as soon as it has prepared its + * mediaplayer. + */ + public void setVideoSurface(SurfaceHolder sh) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting display"); + mediaPlayer.setVideoSurface(sh); + } + + /** + * Called when the surface holder of the mediaplayer has to be changed. + */ + private void resetVideoSurface() { + taskManager.cancelPositionSaver(); + mediaPlayer.resetVideoSurface(); + } + + public void notifyVideoSurfaceAbandoned() { + stopForeground(true); + mediaPlayer.resetVideoSurface(); + } + + private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); + } + + @Override + public void onSleepTimerExpired() { + mediaPlayer.pause(true, true); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + + @Override + public void onWidgetUpdaterTick() { + updateWidget(); + } + + @Override + public void onChapterLoaded(Playable media) { + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + } + }; + + private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + currentMediaType = mediaPlayer.getCurrentMediaType(); + switch (newInfo.playerStatus) { + case INITIALIZED: + writePlaybackPreferences(); + break; + + case PREPARED: + taskManager.startChapterLoader(newInfo.playable); + break; + + case PAUSED: + taskManager.cancelPositionSaver(); + saveCurrentPosition(false, 0); + taskManager.cancelWidgetUpdater(); + stopForeground(true); + break; + + case STOPPED: + //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); + //stopSelf(); + break; + + case PLAYING: + if (AppConfig.DEBUG) + Log.d(TAG, "Audiofocus successfully requested"); + if (AppConfig.DEBUG) + Log.d(TAG, "Resuming/Starting playback"); + + taskManager.startPositionSaver(); + taskManager.startWidgetUpdater(); + setupNotification(newInfo); + break; + + } + + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + updateWidget(); + refreshRemoteControlClientState(newInfo); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); + } + + @Override + public void shouldStop() { + stopSelf(); + } + + @Override + public void playbackSpeedChanged(float s) { + sendNotificationBroadcast( + NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); + } + + @Override + public void onBufferingUpdate(int percent) { + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); + } + + @Override + public boolean onMediaPlayerInfo(int code) { + switch (code) { + case MediaPlayer.MEDIA_INFO_BUFFERING_START: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); + return true; + case MediaPlayer.MEDIA_INFO_BUFFERING_END: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); + return true; + default: + return false; + } + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + final String TAG = "PlaybackService.onErrorListener"; + Log.w(TAG, "An error has occured: " + what + " " + extra); + if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, false); + } + sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); + setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); + stopSelf(); + return true; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + PlaybackService.this.endPlayback(true); + return true; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return remoteControlClient; + } + }; + + private void endPlayback(boolean playNextEpisode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Playback ended"); + + final Playable media = mediaPlayer.getPSMPInfo().playable; + if (media == null) { + Log.e(TAG, "Cannot end playback: media was null"); + return; + } + + taskManager.cancelPositionSaver(); + + boolean isInQueue = false; + FeedItem nextItem = null; + + if (media instanceof FeedMedia) { + FeedItem item = ((FeedMedia) media).getItem(); + DBWriter.markItemRead(PlaybackService.this, item, true, true); + + try { + final List<FeedItem> queue = taskManager.getQueue(); + isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); + nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); + } catch (InterruptedException e) { + e.printStackTrace(); + // isInQueue remains false + } + if (isInQueue) { + DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); + } + DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + } + + // Load next episode if previous episode was in the queue and if there + // is an episode in the queue left. + // Start playback immediately if continuous playback is enabled + Playable nextMedia = null; + boolean loadNextItem = isInQueue && nextItem != null; + playNextEpisode = playNextEpisode && loadNextItem + && UserPreferences.isFollowQueue(); + if (loadNextItem) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading next item in queue"); + nextMedia = nextItem.getMedia(); + } + final boolean prepareImmediately; + final boolean startWhenPrepared; + final boolean stream; + + if (playNextEpisode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Playback of next episode will start immediately."); + prepareImmediately = startWhenPrepared = true; + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "No more episodes available to play"); + + prepareImmediately = startWhenPrepared = false; + stopForeground(true); + stopWidgetUpdater(); + } + + writePlaybackPreferences(); + if (nextMedia != null) { + stream = !media.localFileAvailable(); + mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); + } else { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + //stopSelf(); + } + } + + public void setSleepTimer(long waitingTime) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + taskManager.setSleepTimer(waitingTime); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + public void disableSleepTimer() { + taskManager.disableSleepTimer(); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + + private void writePlaybackPreferences() { + if (AppConfig.DEBUG) + Log.d(TAG, "Writing playback preferences"); + + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + MediaType mediaType = mediaPlayer.getCurrentMediaType(); + boolean stream = mediaPlayer.isStreaming(); + + if (info.playable != null) { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + info.playable.getPlayableType()); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, + stream); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, + mediaType == MediaType.VIDEO); + if (info.playable instanceof FeedMedia) { + FeedMedia fMedia = (FeedMedia) info.playable; + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + fMedia.getItem().getFeed().getId()); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + fMedia.getId()); + } else { + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + info.playable.writeToPreferences(editor); + } else { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + + editor.commit(); + } + + /** + * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. + */ + private void postStatusUpdateIntent() { + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + } + + private void sendNotificationBroadcast(int type, int code) { + Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); + intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); + intent.putExtra(EXTRA_NOTIFICATION_CODE, code); + sendBroadcast(intent); + } + + /** + * Used by setupNotification to load notification data in another thread. + */ + private AsyncTask<Void, Void, Void> notificationSetupTask; + + /** + * Prepares notification and starts the service in the foreground. + */ + @SuppressLint("NewApi") + private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { + final PendingIntent pIntent = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT); + + if (notificationSetupTask != null) { + notificationSetupTask.cancel(true); + } + notificationSetupTask = new AsyncTask<Void, Void, Void>() { + Bitmap icon = null; + + @Override + protected Void doInBackground(Void... params) { + if (AppConfig.DEBUG) + Log.d(TAG, "Starting background work"); + if (android.os.Build.VERSION.SDK_INT >= 11) { + if (info.playable != null) { + int iconSize = getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + icon = BitmapDecoder + .decodeBitmapFromWorkerTaskResource(iconSize, + info.playable); + } + + } + if (icon == null) { + icon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_stat_antenna); + } + + return null; + } + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING + && info.playable != null) { + String contentText = info.playable.getFeedTitle(); + String contentTitle = info.playable.getEpisodeTitle(); + Notification notification = null; + if (android.os.Build.VERSION.SDK_INT >= 16) { + Intent pauseButtonIntent = new Intent( + PlaybackService.this, PlaybackService.class); + pauseButtonIntent.putExtra( + MediaButtonReceiver.EXTRA_KEYCODE, + KeyEvent.KEYCODE_MEDIA_PAUSE); + PendingIntent pauseButtonPendingIntent = PendingIntent + .getService(PlaybackService.this, 0, + pauseButtonIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + Notification.Builder notificationBuilder = new Notification.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setOngoing(true) + .setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_stat_antenna) + .addAction(android.R.drawable.ic_media_pause, + getString(R.string.pause_label), + pauseButtonPendingIntent); + notification = notificationBuilder.build(); + } else { + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText).setOngoing(true) + .setContentIntent(pIntent).setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_stat_antenna); + notification = notificationBuilder.getNotification(); + } + startForeground(NOTIFICATION_ID, notification); + if (AppConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + } + + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + notificationSetupTask + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + notificationSetupTask.execute(); + } + + } + + /** + * Saves the current position of the media file to the DB + * + * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects + * @param deltaPlayedDuration value by which played_duration should be increased. + */ + private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { + int position = getCurrentPosition(); + int duration = getDuration(); + float playbackSpeed = getCurrentPlaybackSpeed(); + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Saving current position to " + position); + if (updatePlayedDuration && playable instanceof FeedMedia) { + FeedMedia m = (FeedMedia) playable; + FeedItem item = m.getItem(); + m.setPlayedDuration(m.getPlayedDuration() + ((int)(deltaPlayedDuration * playbackSpeed))); + // Auto flattr + if (FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred() && + (m.getPlayedDuration() > UserPreferences.getPlayedDurationAutoflattrThreshold() * duration)) { + + if (AppConfig.DEBUG) + Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) + + " is " + UserPreferences.getPlayedDurationAutoflattrThreshold() * 100 + "% of file duration " + Integer.toString(duration)); + item.getFlattrStatus().setFlattrQueue(); + DBWriter.setFeedItemFlattrStatus(PodcastApp.getInstance(), item, false); + } + } + playable.saveCurrentPosition(PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()), + position); + } + } + + private void stopWidgetUpdater() { + taskManager.cancelWidgetUpdater(); + sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE)); + } + + private void updateWidget() { + PlaybackService.this.sendBroadcast(new Intent( + PlayerWidget.FORCE_WIDGET_UPDATE)); + } + + public boolean sleepTimerActive() { + return taskManager.isSleepTimerActive(); + } + + public long getSleepTimerTimeLeft() { + return taskManager.getSleepTimerTimeLeft(); + } + + @SuppressLint("NewApi") + private RemoteControlClient setupRemoteControlClient() { + if (Build.VERSION.SDK_INT < 14) { + return null; + } + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(new ComponentName(getPackageName(), + MediaButtonReceiver.class.getName())); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( + getApplicationContext(), 0, mediaButtonIntent, 0); + remoteControlClient = new RemoteControlClient(mediaPendingIntent); + int controlFlags; + if (android.os.Build.VERSION.SDK_INT < 16) { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } else { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + remoteControlClient.setTransportControlFlags(controlFlags); + return remoteControlClient; + } + + /** + * Refresh player status and metadata. + */ + @SuppressLint("NewApi") + private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) { + if (android.os.Build.VERSION.SDK_INT >= 14) { + if (remoteControlClient != null) { + switch (info.playerStatus) { + case PLAYING: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + break; + case PAUSED: + case INITIALIZED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + break; + case STOPPED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + break; + case ERROR: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); + break; + default: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); + } + if (info.playable != null) { + MetadataEditor editor = remoteControlClient + .editMetadata(false); + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, + info.playable.getEpisodeTitle()); + + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, + info.playable.getFeedTitle()); + + editor.apply(); + } + if (AppConfig.DEBUG) + Log.d(TAG, "RemoteControlClient state was refreshed"); + } + } + } + + private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { + boolean isPlaying = false; + + if (info.playerStatus == PlayerStatus.PLAYING) { + isPlaying = true; + } + + if (info.playable != null) { + Intent i = new Intent(whatChanged); + i.putExtra("id", 1); + i.putExtra("artist", ""); + i.putExtra("album", info.playable.getFeedTitle()); + i.putExtra("track", info.playable.getEpisodeTitle()); + i.putExtra("playing", isPlaying); + final List<FeedItem> queue = taskManager.getQueueIfLoaded(); + if (queue != null) { + i.putExtra("ListSize", queue.size()); + } + i.putExtra("duration", info.playable.getDuration()); + i.putExtra("position", info.playable.getPosition()); + sendBroadcast(i); + } + } + + /** + * Pauses playback when the headset is disconnected and the preference is + * set + */ + private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { + private static final String TAG = "headsetDisconnected"; + private static final int UNPLUGGED = 0; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && + intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) { + int state = intent.getIntExtra("state", -1); + if (state != -1) { + if (AppConfig.DEBUG) + Log.d(TAG, "Headset plug event. State is " + state); + if (state == UNPLUGGED) { + if (AppConfig.DEBUG) + Log.d(TAG, "Headset was unplugged during playback."); + pauseIfPauseOnDisconnect(); + } + } else { + Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); + } + } + } + }; + + private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + // sound is about to change, eg. bluetooth -> speaker + if (AppConfig.DEBUG) + Log.d(TAG, "Pausing playback because audio is becoming noisy"); + pauseIfPauseOnDisconnect(); + } + // android.media.AUDIO_BECOMING_NOISY + }; + + /** + * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. + */ + private void pauseIfPauseOnDisconnect() { + if (UserPreferences.isPauseOnHeadsetDisconnect()) { + mediaPlayer.pause(true, true); + } + } + + private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && + intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + stopSelf(); + } + } + + }; + + private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && + intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); + mediaPlayer.endPlayback(); + } + } + }; + + public static MediaType getCurrentMediaType() { + return currentMediaType; + } + + public void resume() { + mediaPlayer.resume(); + } + + public void prepare() { + mediaPlayer.prepare(); + } + + public void pause(boolean abandonAudioFocus, boolean reinit) { + mediaPlayer.pause(abandonAudioFocus, reinit); + } + + public void reinit() { + mediaPlayer.reinit(); + } + + public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { + return mediaPlayer.getPSMPInfo(); + } + + public PlayerStatus getStatus() { + return mediaPlayer.getPSMPInfo().playerStatus; + } + + public Playable getPlayable() { + return mediaPlayer.getPSMPInfo().playable; + } + + public void setSpeed(float speed) { + mediaPlayer.setSpeed(speed); + } + + public boolean canSetSpeed() { + return mediaPlayer.canSetSpeed(); + } + + public float getCurrentPlaybackSpeed() { + return mediaPlayer.getPlaybackSpeed(); + } + + public boolean isStartWhenPrepared() { + return mediaPlayer.isStartWhenPrepared(); + } + + public void setStartWhenPrepared(boolean s) { + mediaPlayer.setStartWhenPrepared(s); + } + + + public void seekTo(final int t) { + mediaPlayer.seekTo(t); + } + + + public void seekDelta(final int d) { + mediaPlayer.seekDelta(d); + } + + /** + * @see de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.feed.Chapter) + */ + public void seekToChapter(Chapter c) { + mediaPlayer.seekToChapter(c); + } + + /** + * call getDuration() on mediaplayer or return INVALID_TIME if player is in + * an invalid state. + */ + public int getDuration() { + return mediaPlayer.getDuration(); + } + + /** + * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player + * is in an invalid state. + */ + public int getCurrentPosition() { + return mediaPlayer.getPosition(); + } + + public boolean isStreaming() { + return mediaPlayer.isStreaming(); + } + + public Pair<Integer, Integer> getVideoSize() { + return mediaPlayer.getVideoSize(); + } + + private void setCurrentlyPlayingMedia(long id) { + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id); + editor.commit(); + } +} diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java new file mode 100644 index 000000000..30f6de458 --- /dev/null +++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java @@ -0,0 +1,923 @@ +package de.danoeh.antennapod.service.playback; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.util.Log; +import android.util.Pair; +import android.view.SurfaceHolder; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.MediaType; +import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.util.playback.AudioPlayer; +import de.danoeh.antennapod.util.playback.IPlayer; +import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.util.playback.VideoPlayer; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Manages the MediaPlayer object of the PlaybackService. + */ +public class PlaybackServiceMediaPlayer { + public static final String TAG = "PlaybackServiceMediaPlayer"; + + /** + * Return value of some PSMP methods if the method call failed. + */ + public static final int INVALID_TIME = -1; + + private final AudioManager audioManager; + + private volatile PlayerStatus playerStatus; + private volatile PlayerStatus statusBeforeSeeking; + private volatile IPlayer mediaPlayer; + private volatile Playable media; + + private volatile boolean stream; + private volatile MediaType mediaType; + private volatile AtomicBoolean startWhenPrepared; + private volatile boolean pausedBecauseOfTransientAudiofocusLoss; + private volatile Pair<Integer, Integer> videoSize; + + /** + * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads + * have to wait until these operations have finished. + */ + private final ReentrantLock playerLock; + + private final PSMPCallback callback; + private final Context context; + + private final ThreadPoolExecutor executor; + + public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { + if (context == null) + throw new IllegalArgumentException("context = null"); + if (callback == null) + throw new IllegalArgumentException("callback = null"); + + this.context = context; + this.callback = callback; + this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + this.playerLock = new ReentrantLock(); + this.startWhenPrepared = new AtomicBoolean(false); + executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(), + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + if (AppConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); + } + }); + + mediaPlayer = null; + statusBeforeSeeking = null; + pausedBecauseOfTransientAudiofocusLoss = false; + mediaType = MediaType.UNKNOWN; + playerStatus = PlayerStatus.STOPPED; + videoSize = null; + } + + /** + * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing + * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will + * not do anything. + * Whether playback starts immediately depends on the given parameters. See below for more details. + * <p/> + * States: + * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. + * <p/> + * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If + * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. + * <p/> + * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object + * will enter the ERROR state. + * <p/> + * This method is executed on an internal executor service. + * + * @param playable The Playable object that is supposed to be played. This parameter must not be null. + * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via + * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by + * the Android MediaPlayer via getStreamUrl. + * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the + * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared + * for playback immediately (see 'prepareImmediately' parameter for more details) + * @param prepareImmediately Set to true if the method should also prepare the episode for playback. + */ + public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { + if (playable == null) + throw new IllegalArgumentException("playable = null"); + if (AppConfig.DEBUG) Log.d(TAG, "Play media object."); + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + try { + playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); + } catch (RuntimeException e) { + throw e; + } finally { + playerLock.unlock(); + } + } + }); + } + + /** + * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if + * the given playable parameter is the same object as the currently playing media. + * <p/> + * This method requires the playerLock and is executed on the caller's thread. + * + * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean) + */ + private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { + if (playable == null) + throw new IllegalArgumentException("playable = null"); + if (!playerLock.isHeldByCurrentThread()) + throw new IllegalStateException("method requires playerLock"); + + + if (media != null) { + if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { + // episode is already playing -> ignore method call + return; + } else { + // stop playback of this episode + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { + mediaPlayer.stop(); + } + setPlayerStatus(PlayerStatus.INDETERMINATE, null); + } + } + + this.media = playable; + this.stream = stream; + this.mediaType = media.getMediaType(); + this.videoSize = null; + createMediaPlayer(); + PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared); + setPlayerStatus(PlayerStatus.INITIALIZING, media); + try { + media.loadMetadata(); + if (stream) { + mediaPlayer.setDataSource(media.getStreamUrl()); + } else { + mediaPlayer.setDataSource(media.getLocalMediaUrl()); + } + setPlayerStatus(PlayerStatus.INITIALIZED, media); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT); + } + + if (prepareImmediately) { + setPlayerStatus(PlayerStatus.PREPARING, media); + mediaPlayer.prepare(); + onPrepared(startWhenPrepared); + } + + } catch (Playable.PlayableException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IllegalStateException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + + + /** + * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. + * nothing will happen. + * <p/> + * This method is executed on an internal executor service. + */ + public void resume() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + resumeSync(); + playerLock.unlock(); + } + }); + } + + private void resumeSync() { + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { + int focusGained = audioManager.requestAudioFocus( + audioFocusChangeListener, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + + setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); + mediaPlayer.start(); + if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + setPlayerStatus(PlayerStatus.PLAYING, media); + pausedBecauseOfTransientAudiofocusLoss = false; + if (android.os.Build.VERSION.SDK_INT >= 14) { + RemoteControlClient remoteControlClient = callback.getRemoteControlClient(); + if (remoteControlClient != null) { + audioManager + .registerRemoteControlClient(remoteControlClient); + } + } + audioManager + .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), + MediaButtonReceiver.class.getName())); + media.onPlaybackStart(); + + } else { + if (AppConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); + } + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); + } + } + + + /** + * Saves the current position and pauses playback. Note that, if audiofocus + * is abandoned, the lockscreen controls will also disapear. + * <p/> + * This method is executed on an internal executor service. + * + * @param abandonFocus is true if the service should release audio focus + * @param reinit is true if service should reinit after pausing if the media + * file is being streamed + */ + public void pause(final boolean abandonFocus, final boolean reinit) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus == PlayerStatus.PLAYING) { + if (AppConfig.DEBUG) + Log.d(TAG, "Pausing playback."); + mediaPlayer.pause(); + setPlayerStatus(PlayerStatus.PAUSED, media); + + if (abandonFocus) { + audioManager.abandonAudioFocus(audioFocusChangeListener); + pausedBecauseOfTransientAudiofocusLoss = false; + } + if (stream && reinit) { + reinit(); + } + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Prepared media player for playback if the service is in the INITALIZED + * state. + * <p/> + * This method is executed on an internal executor service. + */ + public void prepare() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus == PlayerStatus.INITIALIZED) { + if (AppConfig.DEBUG) + Log.d(TAG, "Preparing media player"); + setPlayerStatus(PlayerStatus.PREPARING, media); + try { + mediaPlayer.prepare(); + onPrepared(startWhenPrepared.get()); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + playerLock.unlock(); + + } + }); + } + + /** + * Called after media player has been prepared. This method is executed on the caller's thread. + */ + void onPrepared(final boolean startWhenPrepared) { + playerLock.lock(); + + if (playerStatus != PlayerStatus.PREPARING) { + playerLock.unlock(); + throw new IllegalStateException("Player is not in PREPARING state"); + } + + if (AppConfig.DEBUG) + Log.d(TAG, "Resource prepared"); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight()); + } + + if (media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + if (media.getDuration() == 0) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting duration of media"); + media.setDuration(mediaPlayer.getDuration()); + } + setPlayerStatus(PlayerStatus.PREPARED, media); + + if (startWhenPrepared) { + resumeSync(); + } + + playerLock.unlock(); + } + + /** + * Resets the media player and moves it into INITIALIZED state. + * <p/> + * This method is executed on an internal executor service. + */ + public void reinit() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (media != null) { + playMediaObject(media, true, stream, startWhenPrepared.get(), false); + } else if (mediaPlayer != null) { + mediaPlayer.reset(); + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + } + playerLock.unlock(); + } + }); + } + + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * Invalid time values (< 0) will be ignored. + * <p/> + * This method is executed on the caller's thread. + */ + private void seekToSync(int t) { + if (t < 0) { + if (AppConfig.DEBUG) Log.d(TAG, "Received invalid value for t"); + return; + } + playerLock.lock(); + + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + if (stream) { + // statusBeforeSeeking = playerStatus; + // setPlayerStatus(PlayerStatus.SEEKING, media); + } + mediaPlayer.seekTo(t); + + } else if (playerStatus == PlayerStatus.INITIALIZED) { + media.setPosition(t); + startWhenPrepared.set(true); + prepare(); + } + playerLock.unlock(); + } + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * Invalid time values (< 0) will be ignored. + * <p/> + * This method is executed on an internal executor service. + */ + public void seekTo(final int t) { + executor.submit(new Runnable() { + @Override + public void run() { + seekToSync(t); + } + }); + } + + /** + * Seek a specific position from the current position + * + * @param d offset from current position (positive or negative) + */ + public void seekDelta(final int d) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + int currentPosition = getPosition(); + if (currentPosition != INVALID_TIME) { + seekToSync(currentPosition + d); + } else { + Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Seek to the start of the specified chapter. + */ + public void seekToChapter(Chapter c) { + if (c == null) + throw new IllegalArgumentException("c = null"); + seekTo((int) c.getStart()); + } + + /** + * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. + */ + public int getDuration() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getDuration(); + } else if (media != null && media.getDuration() > 0) { + retVal = media.getDuration(); + } + + playerLock.unlock(); + return retVal; + } + + /** + * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. + */ + public int getPosition() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getCurrentPosition(); + } else if (media != null && media.getPosition() > 0) { + retVal = media.getPosition(); + } + + playerLock.unlock(); + return retVal; + } + + public boolean isStartWhenPrepared() { + return startWhenPrepared.get(); + } + + public void setStartWhenPrepared(boolean startWhenPrepared) { + this.startWhenPrepared.set(startWhenPrepared); + } + + /** + * Returns true if the playback speed can be adjusted. This method can also return false if the PSMP object's + * internal MediaPlayer cannot be accessed at the moment. + */ + public boolean canSetSpeed() { + if (!playerLock.tryLock()) { + return false; + } + boolean retVal = false; + if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { + retVal = (mediaPlayer).canSetSpeed(); + } + + playerLock.unlock(); + return retVal; + } + + /** + * Sets the playback speed. + * This method is executed on the caller's thread. + */ + private void setSpeedSync(float speed) { + playerLock.lock(); + if (media != null && media.getMediaType() == MediaType.AUDIO) { + if (mediaPlayer.canSetSpeed()) { + mediaPlayer.setPlaybackSpeed((float) speed); + if (AppConfig.DEBUG) + Log.d(TAG, "Playback speed was set to " + speed); + callback.playbackSpeedChanged(speed); + } + } + playerLock.unlock(); + } + + /** + * Sets the playback speed. + * This method is executed on an internal executor service. + */ + public void setSpeed(final float speed) { + executor.submit(new Runnable() { + @Override + public void run() { + setSpeedSync(speed); + } + }); + } + + /** + * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. + */ + public float getPlaybackSpeed() { + if (!playerLock.tryLock()) { + return 1; + } + + float retVal = 1; + if ((playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) { + retVal = mediaPlayer.getCurrentSpeedMultiplier(); + } + playerLock.unlock(); + return retVal; + } + + public MediaType getCurrentMediaType() { + return mediaType; + } + + public boolean isStreaming() { + return stream; + } + + + /** + * Releases internally used resources. This method should only be called when the object is not used anymore. + */ + public void shutdown() { + executor.shutdown(); + if (mediaPlayer != null) { + mediaPlayer.release(); + } + } + + public void setVideoSurface(final SurfaceHolder surface) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (mediaPlayer != null) { + mediaPlayer.setDisplay(surface); + } + playerLock.unlock(); + } + }); + } + + public void resetVideoSurface() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (AppConfig.DEBUG) + Log.d(TAG, "Resetting video surface"); + mediaPlayer.setDisplay(null); + reinit(); + playerLock.unlock(); + } + }); + } + + /** + * Return width and height of the currently playing video as a pair. + * + * @return Width and height as a Pair or null if the video size could not be determined. The method might still + * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return + * invalid values. + */ + public Pair<Integer, Integer> getVideoSize() { + if (!playerLock.tryLock()) { + // use cached value if lock can't be aquired + return videoSize; + } + Pair<Integer, Integer> res; + if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { + res = null; + } else { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight()); + res = videoSize; + } + playerLock.unlock(); + return res; + } + + /** + * Returns a PSMInfo object that contains information about the current state of the PSMP object. + * + * @return The PSMPInfo object. + */ + public synchronized PSMPInfo getPSMPInfo() { + return new PSMPInfo(playerStatus, media); + } + + /** + * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time + * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). + * <p/> + * This method will notify the callback about the change of the player status (even if the new status is the same + * as the old one). + * + * @param newStatus The new PlayerStatus. This must not be null. + * @param newMedia The new playable object of the PSMP object. This can be null. + */ + private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { + if (newStatus == null) + throw new IllegalArgumentException("newStatus = null"); + if (AppConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); + + this.playerStatus = newStatus; + this.media = newMedia; + callback.statusChanged(new PSMPInfo(playerStatus, media)); + } + + private IPlayer createMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.release(); + } + if (media == null || media.getMediaType() == MediaType.VIDEO) { + mediaPlayer = new VideoPlayer(); + } else { + mediaPlayer = new AudioPlayer(context); + } + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + return setMediaPlayerListeners(mediaPlayer); + } + + private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { + + @Override + public void onAudioFocusChange(final int focusChange) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus"); + pause(true, false); + callback.shouldStop(); + break; + case AudioManager.AUDIOFOCUS_GAIN: + if (AppConfig.DEBUG) + Log.d(TAG, "Gained audio focus"); + if (pausedBecauseOfTransientAudiofocusLoss) // we paused => play now + resume(); + else // we ducked => raise audio level back + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_RAISE, 0); + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (playerStatus == PlayerStatus.PLAYING) { + if (!UserPreferences.shouldPauseForFocusLoss()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, 0); + pausedBecauseOfTransientAudiofocusLoss = false; + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + if (playerStatus == PlayerStatus.PLAYING) { + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + + playerLock.unlock(); + } + }); + + } + }; + + public void endPlayback() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus != PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.INDETERMINATE, media); + } + if (mediaPlayer != null) { + mediaPlayer.reset(); + + } + callback.endPlayback(true); + + playerLock.unlock(); + } + }); + } + + /** + * Holds information about a PSMP object. + */ + public class PSMPInfo { + public PlayerStatus playerStatus; + public Playable playable; + + public PSMPInfo(PlayerStatus playerStatus, Playable playable) { + this.playerStatus = playerStatus; + this.playable = playable; + } + } + + public static interface PSMPCallback { + public void statusChanged(PSMPInfo newInfo); + + public void shouldStop(); + + public void playbackSpeedChanged(float s); + + public void onBufferingUpdate(int percent); + + public boolean onMediaPlayerInfo(int code); + + public boolean onMediaPlayerError(Object inObj, int what, int extra); + + public boolean endPlayback(boolean playNextEpisode); + + public RemoteControlClient getRemoteControlClient(); + } + + private IPlayer setMediaPlayerListeners(IPlayer mp) { + if (mp != null && media != null) { + if (media.getMediaType() == MediaType.AUDIO) { + ((AudioPlayer) mp) + .setOnCompletionListener(audioCompletionListener); + ((AudioPlayer) mp) + .setOnSeekCompleteListener(audioSeekCompleteListener); + ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); + ((AudioPlayer) mp) + .setOnBufferingUpdateListener(audioBufferingUpdateListener); + ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); + } else { + ((VideoPlayer) mp) + .setOnCompletionListener(videoCompletionListener); + ((VideoPlayer) mp) + .setOnSeekCompleteListener(videoSeekCompleteListener); + ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); + ((VideoPlayer) mp) + .setOnBufferingUpdateListener(videoBufferingUpdateListener); + ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); + } + } + return mp; + } + + private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(com.aocate.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(android.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private void genericOnCompletion() { + endPlayback(); + } + + private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, + int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private void genericOnBufferingUpdate(int percent) { + callback.onBufferingUpdate(percent); + } + + private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericInfoListener(what); + } + }; + + private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { + return genericInfoListener(what); + } + }; + + private boolean genericInfoListener(int what) { + return callback.onMediaPlayerInfo(what); + } + + private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericOnError(mp, what, extra); + } + }; + + private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(android.media.MediaPlayer mp, int what, int extra) { + return genericOnError(mp, what, extra); + } + }; + + private boolean genericOnError(Object inObj, int what, int extra) { + return callback.onMediaPlayerError(inObj, what, extra); + } + + private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(com.aocate.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(android.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final void genericSeekCompleteListener() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (playerStatus == PlayerStatus.SEEKING) { + setPlayerStatus(statusBeforeSeeking, media); + } + playerLock.unlock(); + } + }); + } +} diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java new file mode 100644 index 000000000..0c1878e18 --- /dev/null +++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java @@ -0,0 +1,385 @@ +package de.danoeh.antennapod.service.playback; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.EventDistributor; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.util.playback.Playable; + +import java.util.List; +import java.util.concurrent.*; + +/** + * Manages the background tasks of PlaybackSerivce, i.e. + * the sleep timer, the position saver, the widget updater and + * the queue loader. + * <p/> + * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) + * to notify the PlaybackService about updates from the running tasks. + */ +public class PlaybackServiceTaskManager { + private static final String TAG = "PlaybackServiceTaskManager"; + + /** + * Update interval of position saver in milliseconds. + */ + public static final int POSITION_SAVER_WAITING_INTERVAL = 5000; + /** + * Notification interval of widget updater in milliseconds. + */ + public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500; + + private static final int SCHED_EX_POOL_SIZE = 2; + private final ScheduledThreadPoolExecutor schedExecutor; + + private ScheduledFuture positionSaverFuture; + private ScheduledFuture widgetUpdaterFuture; + private ScheduledFuture sleepTimerFuture; + private volatile Future<List<FeedItem>> queueFuture; + private volatile Future chapterLoaderFuture; + + private SleepTimer sleepTimer; + + private final Context context; + private final PSTMCallback callback; + + /** + * Sets up a new PSTM. This method will also start the queue loader task. + * + * @param context + * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. + */ + public PlaybackServiceTaskManager(Context context, PSTMCallback callback) { + if (context == null) + throw new IllegalArgumentException("context must not be null"); + if (callback == null) + throw new IllegalArgumentException("callback must not be null"); + + this.context = context; + this.callback = callback; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + loadQueue(); + EventDistributor.getInstance().register(eventDistributorListener); + } + + private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { + cancelQueueLoader(); + loadQueue(); + } + } + }; + + private synchronized boolean isQueueLoaderActive() { + return queueFuture != null && !queueFuture.isDone(); + } + + private synchronized void cancelQueueLoader() { + if (isQueueLoaderActive()) { + queueFuture.cancel(true); + } + } + + private synchronized void loadQueue() { + if (!isQueueLoaderActive()) { + queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() { + @Override + public List<FeedItem> call() throws Exception { + return DBReader.getQueue(context); + } + }); + } + } + + /** + * Returns the queue if it is already loaded or null if it hasn't been loaded yet. + * In order to wait until the queue has been loaded, use getQueue() + */ + public synchronized List<FeedItem> getQueueIfLoaded() { + if (queueFuture.isDone()) { + try { + return queueFuture.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + return null; + } + + /** + * Returns the queue or waits until the PSTM has loaded the queue from the database. + */ + public synchronized List<FeedItem> getQueue() throws InterruptedException { + try { + return queueFuture.get(); + } catch (ExecutionException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Starts the position saver task. If the position saver is already active, nothing will happen. + */ + public synchronized void startPositionSaver() { + if (!isPositionSaverActive()) { + Runnable positionSaver = new Runnable() { + @Override + public void run() { + callback.positionSaverTick(); + } + }; + positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, + POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); + + if (AppConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); + } + } + + /** + * Returns true if the position saver is currently running. + */ + public synchronized boolean isPositionSaverActive() { + return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone(); + } + + /** + * Cancels the position saver. If the position saver is not running, nothing will happen. + */ + public synchronized void cancelPositionSaver() { + if (isPositionSaverActive()) { + positionSaverFuture.cancel(false); + if (AppConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver"); + } + } + + /** + * Starts the widget updater task. If the widget updater is already active, nothing will happen. + */ + public synchronized void startWidgetUpdater() { + if (!isWidgetUpdaterActive()) { + Runnable widgetUpdater = new Runnable() { + @Override + public void run() { + callback.onWidgetUpdaterTick(); + } + }; + widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, + WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); + + if (AppConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater"); + } else { + if (AppConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored."); + } + } + + /** + * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be + * cancelled first. + * After waitingTime has elapsed, onSleepTimerExpired() will be called. + * + * @throws java.lang.IllegalArgumentException if waitingTime <= 0 + */ + public synchronized void setSleepTimer(long waitingTime) { + if (waitingTime <= 0) + throw new IllegalArgumentException("waitingTime <= 0"); + + if (AppConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + if (isSleepTimerActive()) { + sleepTimerFuture.cancel(true); + } + sleepTimer = new SleepTimer(waitingTime); + sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); + } + + /** + * Returns true if the sleep timer is currently active. + */ + public synchronized boolean isSleepTimerActive() { + return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting; + } + + /** + * Disables the sleep timer. If the sleep timer is not active, nothing will happen. + */ + public synchronized void disableSleepTimer() { + if (isSleepTimerActive()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Disabling sleep timer"); + sleepTimerFuture.cancel(true); + } + } + + /** + * Returns the current sleep timer time or 0 if the sleep timer is not active. + */ + public synchronized long getSleepTimerTimeLeft() { + if (isSleepTimerActive()) { + return sleepTimer.getWaitingTime(); + } else { + return 0; + } + } + + + /** + * Returns true if the widget updater is currently running. + */ + public synchronized boolean isWidgetUpdaterActive() { + return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone(); + } + + /** + * Cancels the widget updater. If the widget updater is not running, nothing will happen. + */ + public synchronized void cancelWidgetUpdater() { + if (isWidgetUpdaterActive()) { + widgetUpdaterFuture.cancel(false); + if (AppConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater"); + } + } + + private synchronized void cancelChapterLoader() { + if (isChapterLoaderActive()) { + chapterLoaderFuture.cancel(true); + } + } + + private synchronized boolean isChapterLoaderActive() { + return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); + } + + /** + * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, + * it will be cancelled first. + * On completion, the callback's onChapterLoaded method will be called. + */ + public synchronized void startChapterLoader(final Playable media) { + if (media == null) + throw new IllegalArgumentException("media = null"); + + if (isChapterLoaderActive()) { + cancelChapterLoader(); + } + + Runnable chapterLoader = new Runnable() { + @Override + public void run() { + if (AppConfig.DEBUG) + Log.d(TAG, "Chapter loader started"); + if (media.getChapters() == null) { + media.loadChapterMarks(); + if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) { + callback.onChapterLoaded(media); + } + } + if (AppConfig.DEBUG) + Log.d(TAG, "Chapter loader stopped"); + } + }; + chapterLoaderFuture = schedExecutor.submit(chapterLoader); + } + + + /** + * Cancels all tasks. The PSTM will be in the initial state after execution of this method. + */ + public synchronized void cancelAllTasks() { + cancelPositionSaver(); + cancelWidgetUpdater(); + disableSleepTimer(); + cancelQueueLoader(); + cancelChapterLoader(); + } + + /** + * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after + * execution of this method. + */ + public synchronized void shutdown() { + EventDistributor.getInstance().unregister(eventDistributorListener); + cancelAllTasks(); + schedExecutor.shutdown(); + } + + /** + * Sleeps for a given time and then pauses playback. + */ + private class SleepTimer implements Runnable { + private static final String TAG = "SleepTimer"; + private static final long UPDATE_INTERVALL = 1000L; + private volatile long waitingTime; + private volatile boolean isWaiting; + + public SleepTimer(long waitingTime) { + super(); + this.waitingTime = waitingTime; + isWaiting = true; + } + + @Override + public void run() { + if (AppConfig.DEBUG) + Log.d(TAG, "Starting"); + while (waitingTime > 0) { + try { + Thread.sleep(UPDATE_INTERVALL); + waitingTime -= UPDATE_INTERVALL; + + if (waitingTime <= 0) { + if (AppConfig.DEBUG) + Log.d(TAG, "Waiting completed"); + postExecute(); + if (!Thread.currentThread().isInterrupted()) { + callback.onSleepTimerExpired(); + } + + } + } catch (InterruptedException e) { + Log.d(TAG, "Thread was interrupted while waiting"); + break; + } + } + postExecute(); + } + + protected void postExecute() { + isWaiting = false; + } + + public long getWaitingTime() { + return waitingTime; + } + + public boolean isWaiting() { + return isWaiting; + } + + } + + public static interface PSTMCallback { + void positionSaverTick(); + + void onSleepTimerExpired(); + + void onWidgetUpdaterTick(); + + void onChapterLoaded(Playable media); + } +} diff --git a/src/de/danoeh/antennapod/service/PlayerStatus.java b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java index fbf5b1505..3d2b4ad39 100644 --- a/src/de/danoeh/antennapod/service/PlayerStatus.java +++ b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java @@ -1,14 +1,14 @@ -package de.danoeh.antennapod.service; +package de.danoeh.antennapod.service.playback; public enum PlayerStatus { + INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state. ERROR, PREPARING, PAUSED, PLAYING, STOPPED, PREPARED, - SEEKING, - AWAITING_VIDEO_SURFACE, // player has been initialized and the media type to be played is a video. + SEEKING, INITIALIZING, // playback service is loading the Playable's metadata INITIALIZED // playback service was started, data source of media player was set. } diff --git a/src/de/danoeh/antennapod/service/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java index 475af9655..90ad7a9fa 100644 --- a/src/de/danoeh/antennapod/service/PlayerWidgetService.java +++ b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.service; +package de.danoeh.antennapod.service.playback; import android.app.PendingIntent; import android.app.Service; @@ -6,6 +6,7 @@ import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; +import android.os.Build; import android.os.IBinder; import android.util.Log; import android.view.KeyEvent; @@ -72,9 +73,11 @@ public class PlayerWidgetService extends Service { } private void updateViews() { + if (playbackService == null) { + return; + } isUpdating = true; - if (AppConfig.DEBUG) - Log.d(TAG, "Updating widget views"); + ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); AppWidgetManager manager = AppWidgetManager.getInstance(this); RemoteViews views = new RemoteViews(getPackageName(), @@ -83,8 +86,8 @@ public class PlayerWidgetService extends Service { PlaybackService.getPlayerActivityIntent(this), 0); views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer); - if (playbackService != null && playbackService.getMedia() != null) { - Playable media = playbackService.getMedia(); + final Playable media = playbackService.getPlayable(); + if (playbackService != null && media != null) { PlayerStatus status = playbackService.getStatus(); views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); @@ -95,14 +98,18 @@ public class PlayerWidgetService extends Service { views.setTextViewText(R.id.txtvProgress, progressString); } views.setImageViewResource(R.id.butPlay, R.drawable.av_pause_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); + } } else { views.setImageViewResource(R.id.butPlay, R.drawable.av_play_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); + } } views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent()); } else { - if (AppConfig.DEBUG) - Log.d(TAG, "No media playing. Displaying defaultt views"); views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE); views.setTextViewText(R.id.txtvTitle, this.getString(R.string.no_media_playing_label)); @@ -126,8 +133,8 @@ public class PlayerWidgetService extends Service { } private String getProgressString(PlaybackService ps) { - int position = ps.getCurrentPositionSafe(); - int duration = ps.getDurationSafe(); + int position = ps.getCurrentPosition(); + int duration = ps.getDuration(); if (position != PlaybackService.INVALID_TIME && duration != PlaybackService.INVALID_TIME) { return Converter.getDurationStringLong(position) + " / " diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java index 8aa93d7ed..ccbf6646f 100644 --- a/src/de/danoeh/antennapod/storage/DBReader.java +++ b/src/de/danoeh/antennapod/storage/DBReader.java @@ -1,20 +1,23 @@ package de.danoeh.antennapod.storage; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.service.download.*; +import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.comparator.DownloadStatusComparator; import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; +import de.danoeh.antennapod.util.comparator.PlaybackCompletionDateComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; /** * Provides methods for reading data from the AntennaPod database. @@ -123,6 +126,7 @@ public final class DBReader { * Takes a list of FeedItems and loads their corresponding Feed-objects from the database. * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will * not find the correct feed of an item. + * * @param context A context that is used for opening a database connection. * @param items The FeedItems whose Feed-objects should be loaded. */ @@ -210,7 +214,9 @@ public final class DBReader { .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0)); item.setItemIdentifier(itemlistCursor .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); - + item.setFlattrStatus(new FlattrStatus(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS))); + // extract chapters boolean hasSimpleChapters = itemlistCursor .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0; @@ -301,7 +307,8 @@ public final class DBReader { cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, - playbackCompletionDate); + playbackCompletionDate, + cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX)); } private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, @@ -329,7 +336,8 @@ public final class DBReader { image, cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL), cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0); + cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0, + new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS))); if (image != null) { image.setFeed(feed); @@ -515,8 +523,9 @@ public final class DBReader { List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor); loadFeedDataOfFeedItemlist(context, items); itemCursor.close(); - adapter.close(); + + Collections.sort(items, new PlaybackCompletionDateComparator()); return items; } @@ -774,4 +783,48 @@ public final class DBReader { return media; } + + /** + * Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems. + * + * @param context A context that is used for opening a database connection. + * @return The flattr queue as a List. + */ + public static List<FlattrThing> getFlattrQueue(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FlattrThing> result = new ArrayList<FlattrThing>(); + + // load feeds + Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor(); + if (feedCursor.moveToFirst()) { + do { + result.add(extractFeedFromCursorRow(adapter, feedCursor)); + } while (feedCursor.moveToNext()); + } + feedCursor.close(); + + //load feed items + Cursor feedItemCursor = adapter.getFeedItemsInFlattrQueueCursor(); + result.addAll(extractItemlistFromCursor(adapter, feedItemCursor)); + feedItemCursor.close(); + + adapter.close(); + Log.d(TAG, "Returning flattrQueueIterator for queue with " + result.size() + " items."); + return result; + } + + + /** + * Returns true if the flattr queue is empty. + * + * @param context A context that is used for opening a database connection. + */ + public static boolean getFlattrQueueEmpty(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + boolean empty = adapter.getFlattrQueueSize() == 0; + adapter.close(); + return empty; + } } diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java index 26d5c712a..8ef5f0888 100644 --- a/src/de/danoeh/antennapod/storage/DBTasks.java +++ b/src/de/danoeh/antennapod/storage/DBTasks.java @@ -4,17 +4,20 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.util.Log; +import de.danoeh.antennapod.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.asynctask.FlattrStatusFetcher; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.GpodnetSyncService; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.NetworkUtils; import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.util.exception.MediaFileNotFoundException; +import de.danoeh.antennapod.util.flattr.FlattrUtils; import java.util.*; import java.util.concurrent.*; @@ -151,6 +154,12 @@ public final class DBTasks { } isRefreshing.set(false); + if (AppConfig.DEBUG) Log.d(TAG, "Flattring all pending things."); + new FlattrClickWorker(context, FlattrClickWorker.FLATTR_NOTIFICATION).executeSync(); // flattr pending things + + if (AppConfig.DEBUG) Log.d(TAG, "Fetching flattr status."); + new FlattrStatusFetcher(context).start(); + GpodnetSyncService.sendSyncIntent(context); autodownloadUndownloadedItems(context); } @@ -797,4 +806,34 @@ public final class DBTasks { } } + /** + * Adds the given FeedItem to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * @param context + * @param item + */ + public static void flattrItemIfLoggedIn(Context context, FeedItem item) { + if (FlattrUtils.hasToken()) { + item.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, item, true); + } else { + FlattrUtils.showNoTokenDialog(context, item.getPaymentLink()); + } + } + + /** + * Adds the given Feed to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * @param context + * @param feed + */ + public static void flattrFeedIfLoggedIn(Context context, Feed feed) { + if (FlattrUtils.hasToken()) { + feed.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, feed, true); + } else { + FlattrUtils.showNoTokenDialog(context, feed.getPaymentLink()); + } + } + } diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java index 6be1a5327..444e9ea0c 100644 --- a/src/de/danoeh/antennapod/storage/DBWriter.java +++ b/src/de/danoeh/antennapod/storage/DBWriter.java @@ -1,15 +1,5 @@ package de.danoeh.antennapod.storage; -import java.io.File; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; - import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -17,13 +7,29 @@ import android.database.Cursor; import android.preference.PreferenceManager; import android.util.Log; import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.preferences.GpodnetPreferences; import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.service.GpodnetSyncService; -import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.util.flattr.FlattrThing; +import de.danoeh.antennapod.util.flattr.SimpleFlattrThing; +import org.shredzone.flattr4j.model.Flattr; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; /** * Provides methods for writing data to AntennaPod's database. @@ -222,6 +228,9 @@ public class DBWriter { if (AppConfig.DEBUG) Log.d(TAG, "Adding new item to playback history"); media.setPlaybackCompletionDate(new Date()); + // reset played_duration to 0 so that it behaves correctly when the episode is played again + media.setPlayedDuration(0); + PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); adapter.setFeedMediaPlaybackCompletionDate(media); @@ -446,7 +455,7 @@ public class DBWriter { }); } - + /** * Moves the specified item to the top of the queue. * @@ -472,7 +481,7 @@ public class DBWriter { } }); } - + /** * Moves the specified item to the bottom of the queue. * @@ -491,7 +500,7 @@ public class DBWriter { for (long id : queueIdList) { if (id == itemId) { moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1, - broadcastUpdate); + broadcastUpdate); return; } currentLocation++; @@ -500,7 +509,7 @@ public class DBWriter { } }); } - + /** * Changes the position of a FeedItem in the queue. * @@ -524,7 +533,7 @@ public class DBWriter { /** * Changes the position of a FeedItem in the queue. - * + * <p/> * This function must be run using the ExecutorService (dbExec). * * @param context A context that is used for opening a database connection. @@ -535,7 +544,7 @@ public class DBWriter { * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) */ private static void moveQueueItemHelper(final Context context, final int from, - final int to, final boolean broadcastUpdate) { + final int to, final boolean broadcastUpdate) { final PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); final List<FeedItem> queue = DBReader @@ -823,4 +832,125 @@ public class DBWriter { } return false; } + + /** + * Saves the FlattrStatus of a FeedItem object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + public static Future<?> setFeedItemFlattrStatus(final Context context, + final FeedItem item, + final boolean startFlattrClickWorker) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemFlattrStatus(item); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync(); + } + } + }); + } + + /** + * Saves the FlattrStatus of a Feed object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + private static Future<?> setFeedFlattrStatus(final Context context, + final Feed feed, + final boolean startFlattrClickWorker) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedFlattrStatus(feed); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context, FlattrClickWorker.FLATTR_TOAST).executeAsync(); + } + } + }); + } + + /** + * format an url for querying the database + * (postfix a / and apply percent-encoding) + */ + private static String formatURIForQuery(String uri) { + try { + return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getMessage()); + return ""; + } + } + + + /** + * Set flattr status of the passed thing (either a FeedItem or a Feed) + * + * @param context + * @param thing + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + * @return + */ + public static Future<?> setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) { + // must propagate this to back db + if (thing instanceof FeedItem) + return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker); + else if (thing instanceof Feed) + return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker); + else if (thing instanceof SimpleFlattrThing) { + } // SimpleFlattrThings are generated on the fly and do not have DB backing + else + Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing"); + + return null; + } + + /** + * Reset flattr status to unflattrd for all items + */ + public static Future<?> clearAllFlattrStatus(final Context context) { + Log.d(TAG, "clearAllFlattrStatus()"); + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearAllFlattrStatus(); + adapter.close(); + } + }); + } + + /** + * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp, + * where the information has been retrieved from the flattr API + */ + public static Future<?> setFlattredStatus(final Context context, final List<Flattr> flattrList) { + Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items"); + // clear flattr status in db + clearAllFlattrStatus(context); + + // submit list with flattred things having normalized URLs to db + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (Flattr flattr : flattrList) { + adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime())); + } + adapter.close(); + } + }); + } } diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index 068f80ded..b44883744 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -1,8 +1,5 @@ package de.danoeh.antennapod.storage; -import java.util.Arrays; -import java.util.List; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -16,6 +13,10 @@ import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.util.flattr.FlattrStatus; + +import java.util.Arrays; +import java.util.List; // TODO Remove media column from feeditem table @@ -24,7 +25,7 @@ import de.danoeh.antennapod.service.download.DownloadStatus; */ public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; - private static final int DATABASE_VERSION = 10; + private static final int DATABASE_VERSION = 11; public static final String DATABASE_NAME = "Antennapod.db"; /** @@ -54,6 +55,7 @@ public class PodDBAdapter { public static final int KEY_IMAGE_INDEX = 11; public static final int KEY_TYPE_INDEX = 12; public static final int KEY_FEED_IDENTIFIER_INDEX = 13; + public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14; // ----------- FeedItem indices public static final int KEY_CONTENT_ENCODED_INDEX = 2; public static final int KEY_PUBDATE_INDEX = 3; @@ -62,6 +64,7 @@ public class PodDBAdapter { public static final int KEY_FEED_INDEX = 9; public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; + public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12; // ---------- FeedMedia indices public static final int KEY_DURATION_INDEX = 1; public static final int KEY_POSITION_INDEX = 5; @@ -69,6 +72,7 @@ public class PodDBAdapter { public static final int KEY_MIME_TYPE_INDEX = 7; public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; public static final int KEY_MEDIA_FEEDITEM_INDEX = 9; + public static final int KEY_PLAYED_DURATION_INDEX = 10; // --------- Download log indices public static final int KEY_FEEDFILE_INDEX = 1; public static final int KEY_FEEDFILETYPE_INDEX = 2; @@ -119,12 +123,14 @@ public class PodDBAdapter { public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; public static final String KEY_TYPE = "type"; public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; + public static final String KEY_FLATTR_STATUS = "flattr_status"; public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; public static final String KEY_REASON_DETAILED = "reason_detailed"; public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; public static final String KEY_CHAPTER_TYPE = "type"; public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; public static final String KEY_AUTO_DOWNLOAD = "auto_download"; + public static final String KEY_PLAYED_DURATION = "played_duration"; // Table names public static final String TABLE_NAME_FEEDS = "Feeds"; @@ -146,7 +152,8 @@ public class PodDBAdapter { + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," - + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1)"; + + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," + + KEY_FLATTR_STATUS + " INTEGER)"; private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -154,7 +161,8 @@ public class PodDBAdapter { + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," - + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT)"; + + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + + KEY_FLATTR_STATUS + " INTEGER)"; private static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE " + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -167,7 +175,8 @@ public class PodDBAdapter { + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," - + KEY_FEEDITEM + " INTEGER)"; + + KEY_FEEDITEM + " INTEGER," + + KEY_PLAYED_DURATION + " INTEGER)"; private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE @@ -208,6 +217,7 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_TYPE, TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, + TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS }; // column indices for FEED_SEL_STD @@ -226,6 +236,7 @@ public class PodDBAdapter { public static final int IDX_FEED_SEL_STD_TYPE = 12; public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13; public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14; + public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15; /** @@ -241,7 +252,8 @@ public class PodDBAdapter { TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA, TABLE_NAME_FEED_ITEMS + "." + KEY_FEED, TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, - TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER}; + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER, + TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS}; /** * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries. @@ -265,6 +277,7 @@ public class PodDBAdapter { public static final int IDX_FI_SMALL_FEED = 7; public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8; public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9; + public static final int IDX_FI_SMALL_FLATTR_STATUS = 10; /** * Select id, description and content-encoded column from feeditems. @@ -346,6 +359,10 @@ public class PodDBAdapter { values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime()); values.put(KEY_TYPE, feed.getType()); values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); + + Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong()); + + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); if (feed.getId() == 0) { // Create new entry if (AppConfig.DEBUG) @@ -435,6 +452,7 @@ public class PodDBAdapter { ContentValues values = new ContentValues(); values.put(KEY_POSITION, media.getPosition()); values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(media.getId())}); } else { @@ -446,6 +464,7 @@ public class PodDBAdapter { if (media.getId() != 0) { ContentValues values = new ContentValues(); values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(media.getId())}); } else { @@ -470,6 +489,56 @@ public class PodDBAdapter { } /** + * Update the flattr status of a feed + */ + public void setFeedFlattrStatus(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); + } + + /** + * Get all feeds in the flattr queue. + */ + public Cursor getFeedsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)},null, null, null); + } + + /** + * Get all feed items in the flattr queue. + */ + public Cursor getFeedItemsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)},null, null, null); + } + + /** + * Counts feeds and feed items in the flattr queue + */ + public int getFlattrQueueSize() { + int res = 0; + Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res = c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds"); + } + c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res += c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items"); + } + + return res; + } + + /** * Updates the download URL of a Feed. */ public void setFeedDownloadUrl(String original, String updated) { @@ -496,6 +565,63 @@ public class PodDBAdapter { } /** + * Update the flattr status of a FeedItem + */ + public void setFeedItemFlattrStatus(FeedItem feedItem) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feedItem.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(feedItem.getId())}); + } + + /** + * Update the flattr status of a feed or feed item specified by its payment link + * and the new flattr status to use + */ + public void setItemFlattrStatus(String url, FlattrStatus status) + { + //Log.d(TAG, "setItemFlattrStatus(" + url + ") = " + status.toString()); + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, status.toLong()); + + // regexps in sqlite would be neat! + String[] query_urls = new String[]{ + "*" + url + "&*", + "*" + url + "%2F&*", + "*" + url + "", + "*" + url + "%2F" + }; + + if (db.update(TABLE_NAME_FEEDS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls) > 0) + { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in Feeds table"); + return; + } + if (db.update(TABLE_NAME_FEED_ITEMS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls) > 0) + { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in FeedsItems table"); + } + } + + /** + * Reset flattr status to unflattrd for all items + */ + public void clearAllFlattrStatus() + { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, 0); + db.update(TABLE_NAME_FEEDS, values, null, null); + db.update(TABLE_NAME_FEED_ITEMS, values, null, null); + } + + /** * Inserts or updates a feeditem entry * * @param item The FeedItem @@ -522,6 +648,7 @@ public class PodDBAdapter { values.put(KEY_READ, item.isRead()); values.put(KEY_HAS_CHAPTERS, item.getChapters() != null); values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); + values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong()); if (item.getId() == 0) { item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); } else { @@ -711,7 +838,7 @@ public class PodDBAdapter { */ public final Cursor getAllFeedsCursor() { Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null, - KEY_TITLE + " ASC"); + KEY_TITLE + " COLLATE NOCASE ASC"); return c; } @@ -849,7 +976,7 @@ public class PodDBAdapter { /** * Returns a cursor which contains feed media objects with a playback - * completion date in descending order. + * completion date in ascending order. * * @param limit The maximum row count of the returned cursor. Must be an * integer >= 0. @@ -860,8 +987,8 @@ public class PodDBAdapter { throw new IllegalArgumentException("Limit must be >= 0"); } Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, - KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null, - null, KEY_PLAYBACK_COMPLETION_DATE + " DESC LIMIT " + limit); + KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null, + null, null); return c; } @@ -1072,7 +1199,7 @@ public class PodDBAdapter { " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + - " ON Feeds.id = feed ORDER BY Feeds.title;"; + " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;"; public Cursor getFeedStatisticsCursor() { return db.rawQuery(FEED_STATISTICS_QUERY, null); @@ -1170,6 +1297,17 @@ public class PodDBAdapter { + " ADD COLUMN " + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1"); } + if (oldVersion <= 10) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_PLAYED_DURATION + + " INTEGER"); + } } } } diff --git a/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java b/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java index 5a2c6005e..3eb49172d 100644 --- a/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java +++ b/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java @@ -101,7 +101,10 @@ public class NSRSS20 extends Namespace { } if (top.equals(GUID) && second.equals(ITEM)) { - state.getCurrentItem().setItemIdentifier(content); + // some feed creators include an empty or non-standard guid-element in their feed, which should be ignored + if (!content.isEmpty()) { + state.getCurrentItem().setItemIdentifier(content); + } } else if (top.equals(TITLE)) { if (second.equals(ITEM)) { state.getCurrentItem().setTitle(content); diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java b/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java new file mode 100644 index 000000000..a1d6d3bc4 --- /dev/null +++ b/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.util.flattr; + +import java.util.Calendar; + +public class FlattrStatus { + public static final int STATUS_UNFLATTERED = 0; + public static final int STATUS_QUEUE = 1; + public static final int STATUS_FLATTRED = 2; + + private int status = STATUS_UNFLATTERED; + private Calendar lastFlattred; + + public FlattrStatus() { + status = STATUS_UNFLATTERED; + lastFlattred = Calendar.getInstance(); + } + + public FlattrStatus(long status) { + lastFlattred = Calendar.getInstance(); + fromLong(status); + } + + public void setFlattred() { + status = STATUS_FLATTRED; + lastFlattred = Calendar.getInstance(); + } + + public void setUnflattred() { + status = STATUS_UNFLATTERED; + } + + public boolean getUnflattred() { + return status == STATUS_UNFLATTERED; + } + + public void setFlattrQueue() { + if (flattrable()) + status = STATUS_QUEUE; + } + + public void fromLong(long status) { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + this.status = (int) status; + else { + this.status = STATUS_FLATTRED; + lastFlattred.setTimeInMillis(status); + } + } + + public long toLong() { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + return status; + else { + return lastFlattred.getTimeInMillis(); + } + } + + public boolean flattrable() { + Calendar firstOfMonth = Calendar.getInstance(); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) ); + } + + public boolean getFlattrQueue() { + return status == STATUS_QUEUE; + } +} diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrThing.java b/src/de/danoeh/antennapod/util/flattr/FlattrThing.java new file mode 100644 index 000000000..872132517 --- /dev/null +++ b/src/de/danoeh/antennapod/util/flattr/FlattrThing.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.util.flattr; + +import de.danoeh.antennapod.util.flattr.FlattrStatus; + +public interface FlattrThing { + public String getTitle(); + public String getPaymentLink(); + public FlattrStatus getFlattrStatus(); +} diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java b/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java index ca2c9eb0f..215e67e55 100644 --- a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java +++ b/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java @@ -1,9 +1,16 @@ package de.danoeh.antennapod.util.flattr; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; import java.util.EnumSet; +import java.util.List; +import java.util.ListIterator; +import java.util.TimeZone; import org.shredzone.flattr4j.FlattrService; import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; import org.shredzone.flattr4j.model.Thing; import org.shredzone.flattr4j.oauth.AccessToken; import org.shredzone.flattr4j.oauth.AndroidAuthenticator; @@ -23,6 +30,7 @@ import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FlattrAuthActivity; import de.danoeh.antennapod.asynctask.FlattrTokenFetcher; +import de.danoeh.antennapod.storage.DBWriter; /** Utility methods for doing something with flattr. */ @@ -119,6 +127,58 @@ public class FlattrUtils { Log.e(TAG, "clickUrl was called with null access token"); } } + + public static List<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; + + int page = 0; + do { + myFlattrs.ensureCapacity(FLATTR_COUNT*(page+1)); + + for (Flattr fl: fs.getMyFlattrs(FLATTR_COUNT, page)) { + if (fl.getCreated().after(firstOfMonthDate)) + myFlattrs.add(fl); + else + break; + } + page++; + } + while (myFlattrs.get(myFlattrs.size()-1).getCreated().after( firstOfMonthDate ) && page < FLATTR_MAXPAGE); + + if (AppConfig.DEBUG) { + Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate); + + for (Flattr fl: myFlattrs) { + Thing thing = fl.getThing(); + Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated()); + } + } + + } else { + Log.e(TAG, "retrieveFlattrdThings was called with null access token"); + } + + return myFlattrs; + } public static void handleCallback(Context context, Uri uri) { AndroidAuthenticator auth = createAuthenticator(); @@ -131,7 +191,8 @@ public class FlattrUtils { deleteToken(); FlattrServiceCreator.deleteFlattrService(); showRevokeDialog(context); - } + DBWriter.clearAllFlattrStatus(context); + } // ------------------------------------------------ DIALOGS diff --git a/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java b/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java new file mode 100644 index 000000000..296610871 --- /dev/null +++ b/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.util.flattr; + +/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */ +public class SimpleFlattrThing implements FlattrThing { + public SimpleFlattrThing(String title, String url, FlattrStatus status) + { + this.title = title; + this.url = url; + this.status = status; + } + + public String getTitle() + { + return this.title; + } + + public String getPaymentLink() + { + return this.url; + } + + public FlattrStatus getFlattrStatus() + { + return this.status; + } + + private String title; + private String url; + private FlattrStatus status; +} diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java index e99a733dc..615c1c93e 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java @@ -5,9 +5,8 @@ import android.content.Intent; import android.net.Uri; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackService; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; @@ -15,161 +14,158 @@ import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.ShareUtils; -import java.util.List; - -/** Handles interactions with the FeedItemMenu. */ +/** + * Handles interactions with the FeedItemMenu. + */ public class FeedItemMenuHandler { - private FeedItemMenuHandler() { + private static final String TAG = "FeedItemMenuHandler"; + + private FeedItemMenuHandler() { - } + } - /** - * Used by the MenuHandler to access different types of menus through one - * interface - */ - public interface MenuInterface { - /** - * Implementations of this method should call findItem(id) on their - * menu-object and call setVisibility(visibility) on the returned - * MenuItem object. - */ - abstract void setItemVisibility(int id, boolean visible); - } + /** + * Used by the MenuHandler to access different types of menus through one + * interface + */ + public interface MenuInterface { + /** + * Implementations of this method should call findItem(id) on their + * menu-object and call setVisibility(visibility) on the returned + * MenuItem object. + */ + abstract void setItemVisibility(int id, boolean visible); + } - /** - * This method should be called in the prepare-methods of menus. It changes - * the visibility of the menu items depending on a FeedItem's attributes. - * - * @param mi - * An instance of MenuInterface that the method uses to change a - * MenuItem's visibility - * @param selectedItem - * The FeedItem for which the menu is supposed to be prepared - * @param showExtendedMenu - * True if MenuItems that let the user share information about - * the FeedItem and visit its website should be set visible. This - * parameter should be set to false if the menu space is limited. - * @param queueAccess - * Used for testing if the queue contains the selected item - * @return Returns true if selectedItem is not null. - * */ - public static boolean onPrepareMenu(MenuInterface mi, - FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { + /** + * This method should be called in the prepare-methods of menus. It changes + * the visibility of the menu items depending on a FeedItem's attributes. + * + * @param mi An instance of MenuInterface that the method uses to change a + * MenuItem's visibility + * @param selectedItem The FeedItem for which the menu is supposed to be prepared + * @param showExtendedMenu True if MenuItems that let the user share information about + * the FeedItem and visit its website should be set visible. This + * parameter should be set to false if the menu space is limited. + * @param queueAccess Used for testing if the queue contains the selected item + * @return Returns true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { if (selectedItem == null) { return false; } - DownloadRequester requester = DownloadRequester.getInstance(); - boolean hasMedia = selectedItem.getMedia() != null; - boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); - boolean downloading = hasMedia - && requester.isDownloadingFile(selectedItem.getMedia()); - boolean notLoadedAndNotLoading = hasMedia && (!downloaded) - && (!downloading); - boolean isPlaying = hasMedia - && selectedItem.getState() == FeedItem.State.PLAYING; + DownloadRequester requester = DownloadRequester.getInstance(); + boolean hasMedia = selectedItem.getMedia() != null; + boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); + boolean downloading = hasMedia + && requester.isDownloadingFile(selectedItem.getMedia()); + boolean notLoadedAndNotLoading = hasMedia && (!downloaded) + && (!downloading); + boolean isPlaying = hasMedia + && selectedItem.getState() == FeedItem.State.PLAYING; - FeedItem.State state = selectedItem.getState(); + FeedItem.State state = selectedItem.getState(); - if (!isPlaying) { - mi.setItemVisibility(R.id.skip_episode_item, false); - } - if (!downloaded || isPlaying) { - mi.setItemVisibility(R.id.play_item, false); - mi.setItemVisibility(R.id.remove_item, false); - } - if (!notLoadedAndNotLoading) { - mi.setItemVisibility(R.id.download_item, false); - } - if (!(notLoadedAndNotLoading | downloading) | isPlaying) { - mi.setItemVisibility(R.id.stream_item, false); - } - if (!downloading) { - mi.setItemVisibility(R.id.cancel_download_item, false); - } + if (!isPlaying) { + mi.setItemVisibility(R.id.skip_episode_item, false); + } + if (!downloaded || isPlaying) { + mi.setItemVisibility(R.id.play_item, false); + mi.setItemVisibility(R.id.remove_item, false); + } + if (!notLoadedAndNotLoading) { + mi.setItemVisibility(R.id.download_item, false); + } + if (!(notLoadedAndNotLoading | downloading) | isPlaying) { + mi.setItemVisibility(R.id.stream_item, false); + } + if (!downloading) { + mi.setItemVisibility(R.id.cancel_download_item, false); + } - boolean isInQueue = queueAccess.contains(selectedItem.getId()); - if (!isInQueue || isPlaying) { - mi.setItemVisibility(R.id.remove_from_queue_item, false); - } - if (!(!isInQueue && selectedItem.getMedia() != null)) { - mi.setItemVisibility(R.id.add_to_queue_item, false); - } - if (!showExtendedMenu || selectedItem.getLink() == null) { - mi.setItemVisibility(R.id.share_link_item, false); - } + boolean isInQueue = queueAccess.contains(selectedItem.getId()); + if (!isInQueue || isPlaying) { + mi.setItemVisibility(R.id.remove_from_queue_item, false); + } + if (!(!isInQueue && selectedItem.getMedia() != null)) { + mi.setItemVisibility(R.id.add_to_queue_item, false); + } + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.share_link_item, false); + } - if (!AppConfig.DEBUG - || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { - mi.setItemVisibility(R.id.mark_unread_item, false); - } - if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { - mi.setItemVisibility(R.id.mark_read_item, false); - } + if (!AppConfig.DEBUG + || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { + mi.setItemVisibility(R.id.mark_unread_item, false); + } + if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { + mi.setItemVisibility(R.id.mark_read_item, false); + } - if (!showExtendedMenu || selectedItem.getLink() == null) { - mi.setItemVisibility(R.id.visit_website_item, false); - } + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.visit_website_item, false); + } - if (selectedItem.getPaymentLink() == null) { - mi.setItemVisibility(R.id.support_item, false); - } - return true; - } + if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) { + mi.setItemVisibility(R.id.support_item, false); + } + return true; + } - public static boolean onMenuItemClicked(Context context, int menuItemId, - FeedItem selectedItem) throws DownloadRequestException { - DownloadRequester requester = DownloadRequester.getInstance(); - switch (menuItemId) { - case R.id.skip_episode_item: - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); - break; - case R.id.download_item: - DBTasks.downloadFeedItems(context, selectedItem); - break; - case R.id.play_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - false); - break; - case R.id.remove_item: - DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); - break; - case R.id.cancel_download_item: - requester.cancelDownload(context, selectedItem.getMedia()); - break; - case R.id.mark_read_item: - DBWriter.markItemRead(context, selectedItem, true, true); - break; - case R.id.mark_unread_item: - DBWriter.markItemRead(context, selectedItem, false, true); - break; - case R.id.add_to_queue_item: - DBWriter.addQueueItem(context, selectedItem.getId()); - break; - case R.id.remove_from_queue_item: - DBWriter.removeQueueItem(context, selectedItem.getId(), true); - break; - case R.id.stream_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - true); - break; - case R.id.visit_website_item: - Uri uri = Uri.parse(selectedItem.getLink()); - context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - break; - case R.id.support_item: - new FlattrClickWorker(context, selectedItem.getPaymentLink()) - .executeAsync(); - break; - case R.id.share_link_item: - ShareUtils.shareFeedItemLink(context, selectedItem); - break; - default: - return false; - } - // Refresh menu state + public static boolean onMenuItemClicked(Context context, int menuItemId, + FeedItem selectedItem) throws DownloadRequestException { + DownloadRequester requester = DownloadRequester.getInstance(); + switch (menuItemId) { + case R.id.skip_episode_item: + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + case R.id.download_item: + DBTasks.downloadFeedItems(context, selectedItem); + break; + case R.id.play_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + false); + break; + case R.id.remove_item: + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); + break; + case R.id.cancel_download_item: + requester.cancelDownload(context, selectedItem.getMedia()); + break; + case R.id.mark_read_item: + DBWriter.markItemRead(context, selectedItem, true, true); + break; + case R.id.mark_unread_item: + DBWriter.markItemRead(context, selectedItem, false, true); + break; + case R.id.add_to_queue_item: + DBWriter.addQueueItem(context, selectedItem.getId()); + break; + case R.id.remove_from_queue_item: + DBWriter.removeQueueItem(context, selectedItem.getId(), true); + break; + case R.id.stream_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + true); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedItem.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrItemIfLoggedIn(context, selectedItem); + break; + case R.id.share_link_item: + ShareUtils.shareFeedItemLink(context, selectedItem); + break; + default: + return false; + } + // Refresh menu state - return true; - } + return true; + } } diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java index 27b1a8a8c..537335618 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java @@ -8,6 +8,10 @@ import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FeedInfoActivity; @@ -19,6 +23,7 @@ import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.ShareUtils; +import de.danoeh.antennapod.util.flattr.FlattrStatus; /** Handles interactions with the FeedItemMenu. */ public class FeedMenuHandler { @@ -38,9 +43,10 @@ public class FeedMenuHandler { Log.d(TAG, "Preparing options menu"); menu.findItem(R.id.mark_all_read_item).setVisible( selectedFeed.hasNewItems(true)); - if (selectedFeed.getPaymentLink() != null) { + if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) menu.findItem(R.id.support_item).setVisible(true); - } + else + menu.findItem(R.id.support_item).setVisible(false); MenuItem refresh = menu.findItem(R.id.refresh_item); if (DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFile( @@ -78,8 +84,7 @@ public class FeedMenuHandler { context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); break; case R.id.support_item: - new FlattrClickWorker(context, selectedFeed.getPaymentLink()) - .executeAsync(); + DBTasks.flattrFeedIfLoggedIn(context, selectedFeed); break; case R.id.share_link_item: ShareUtils.shareFeedlink(context, selectedFeed); diff --git a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java index 68d31324d..0945303e4 100644 --- a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java +++ b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java @@ -27,4 +27,9 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { throw new UnsupportedOperationException("Setting display not supported in Audio Player"); } } + + @Override + public void setVideoScalingMode(int mode) { + throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); + } } diff --git a/src/de/danoeh/antennapod/util/playback/IPlayer.java b/src/de/danoeh/antennapod/util/playback/IPlayer.java index ca9b36358..8c1cf4ef4 100644 --- a/src/de/danoeh/antennapod/util/playback/IPlayer.java +++ b/src/de/danoeh/antennapod/util/playback/IPlayer.java @@ -61,4 +61,6 @@ public interface IPlayer { void start(); void stop(); + + public void setVideoScalingMode(int mode); } diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java index 017a0cd5b..0781800aa 100644 --- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java +++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java @@ -1,25 +1,14 @@ package de.danoeh.antennapod.util.playback; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.SharedPreferences; +import android.content.*; import android.content.res.TypedArray; +import android.media.MediaPlayer; import android.os.AsyncTask; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; +import android.util.Pair; import android.view.SurfaceHolder; import android.view.View; import android.view.View.OnClickListener; @@ -30,13 +19,17 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Chapter; import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.feed.MediaType; import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.service.PlaybackService; -import de.danoeh.antennapod.service.PlayerStatus; +import de.danoeh.antennapod.service.playback.PlaybackService; +import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.service.playback.PlayerStatus; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.playback.Playable.PlayableUtils; +import java.util.concurrent.*; + /** * Communicates with the playback service. GUI classes should use this class to * control playback instead of communicating with the PlaybackService directly. @@ -44,10 +37,10 @@ import de.danoeh.antennapod.util.playback.Playable.PlayableUtils; public abstract class PlaybackController { private static final String TAG = "PlaybackController"; - public static final int DEFAULT_SEEK_DELTA = 30000; - public static final int INVALID_TIME = -1; + public static final int DEFAULT_SEEK_DELTA = 30000; + public static final int INVALID_TIME = -1; - private Activity activity; + private final Activity activity; private PlaybackService playbackService; private Playable media; @@ -69,6 +62,8 @@ public abstract class PlaybackController { private boolean reinitOnPause; public PlaybackController(Activity activity, boolean reinitOnPause) { + if (activity == null) + throw new IllegalArgumentException("activity = null"); this.activity = activity; this.reinitOnPause = reinitOnPause; schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, @@ -157,9 +152,6 @@ public abstract class PlaybackController { */ public void pause() { mediaInfoLoaded = false; - if (playbackService != null && playbackService.isPlayingVideo()) { - playbackService.pause(true, true); - } } /** @@ -179,8 +171,9 @@ public abstract class PlaybackController { @Override protected void onPostExecute(Intent serviceIntent) { boolean bound = false; - if (!PlaybackService.isRunning) { + if (!PlaybackService.started) { if (serviceIntent != null) { + if (AppConfig.DEBUG) Log.d(TAG, "Calling start service"); activity.startService(serviceIntent); bound = activity.bindService(serviceIntent, mConnection, 0); } else { @@ -297,7 +290,9 @@ public abstract class PlaybackController { if (AppConfig.DEBUG) Log.d(TAG, "Received statusUpdate Intent."); if (isConnectedToPlaybackService()) { - status = playbackService.getStatus(); + PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); + status = info.playerStatus; + media = info.playable; handleStatus(); } else { Log.w(TAG, @@ -328,10 +323,9 @@ public abstract class PlaybackController { case PlaybackService.NOTIFICATION_TYPE_RELOAD: cancelPositionObserver(); mediaInfoLoaded = false; + queryService(); onReloadNotification(intent.getIntExtra( PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); - queryService(); - break; case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: onSleepTimerUpdate(); @@ -401,37 +395,51 @@ public abstract class PlaybackController { * should be used to update the GUI or start/cancel background threads. */ private void handleStatus() { - TypedArray res = activity.obtainStyledAttributes(new int[]{ - R.attr.av_play, R.attr.av_pause}); - final int playResource = res.getResourceId(0, R.drawable.av_play); - final int pauseResource = res.getResourceId(1, R.drawable.av_pause); - res.recycle(); + final int playResource; + final int pauseResource; + final CharSequence playText = activity.getString(R.string.play_label); + final CharSequence pauseText = activity.getString(R.string.pause_label); + + if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) { + TypedArray res = activity.obtainStyledAttributes(new int[]{ + R.attr.av_play, R.attr.av_pause}); + playResource = res.getResourceId(0, R.drawable.av_play); + pauseResource = res.getResourceId(1, R.drawable.av_pause); + res.recycle(); + } else { + playResource = R.drawable.ic_action_play_over_video; + pauseResource = R.drawable.ic_action_pause_over_video; + } switch (status) { case ERROR: postStatusMsg(R.string.player_error_msg); + handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); break; case PAUSED: clearStatusMsg(); checkMediaInfoLoaded(); cancelPositionObserver(); - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); break; case PLAYING: clearStatusMsg(); checkMediaInfoLoaded(); + if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { + onAwaitingVideoSurface(); + } setupPositionObserver(); - updatePlayButtonAppearance(pauseResource); + updatePlayButtonAppearance(pauseResource, pauseText); break; case PREPARING: postStatusMsg(R.string.player_preparing_msg); checkMediaInfoLoaded(); if (playbackService != null) { if (playbackService.isStartWhenPrepared()) { - updatePlayButtonAppearance(pauseResource); + updatePlayButtonAppearance(pauseResource, pauseText); } else { - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); } } break; @@ -441,32 +449,27 @@ public abstract class PlaybackController { case PREPARED: checkMediaInfoLoaded(); postStatusMsg(R.string.player_ready_msg); - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); break; case SEEKING: postStatusMsg(R.string.player_seeking_msg); break; - case AWAITING_VIDEO_SURFACE: - onAwaitingVideoSurface(); - break; case INITIALIZED: checkMediaInfoLoaded(); clearStatusMsg(); - updatePlayButtonAppearance(playResource); + updatePlayButtonAppearance(playResource, playText); break; } } private void checkMediaInfoLoaded() { - if (!mediaInfoLoaded) { - loadMediaInfo(); - } - mediaInfoLoaded = true; + mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo()); } - private void updatePlayButtonAppearance(int resource) { + private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { ImageButton butPlay = getPlayButton(); butPlay.setImageResource(resource); + butPlay.setContentDescription(contentDescription); } public abstract ImageButton getPlayButton(); @@ -475,7 +478,7 @@ public abstract class PlaybackController { public abstract void clearStatusMsg(); - public abstract void loadMediaInfo(); + public abstract boolean loadMediaInfo(); public abstract void onAwaitingVideoSurface(); @@ -488,7 +491,8 @@ public abstract class PlaybackController { Log.d(TAG, "Querying service info"); if (playbackService != null) { status = playbackService.getStatus(); - media = playbackService.getMedia(); + media = playbackService.getPlayable(); + /* if (media == null) { Log.w(TAG, "PlaybackService has no media object. Trying to restore last played media."); @@ -497,6 +501,7 @@ public abstract class PlaybackController { activity.startService(serviceIntent); } } + */ onServiceQueried(); setupGUI(); @@ -517,7 +522,7 @@ public abstract class PlaybackController { */ public float onSeekBarProgressChanged(SeekBar seekBar, int progress, boolean fromUser, TextView txtvPosition) { - if (fromUser && playbackService != null) { + if (fromUser && playbackService != null && media != null) { float prog = progress / ((float) seekBar.getMax()); int duration = media.getDuration(); txtvPosition.setText(Converter @@ -541,7 +546,7 @@ public abstract class PlaybackController { */ public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { if (playbackService != null) { - playbackService.seek((int) (prog * media.getDuration())); + playbackService.seekTo((int) (prog * media.getDuration())); setupPositionObserver(); } } @@ -557,7 +562,7 @@ public abstract class PlaybackController { break; case PAUSED: case PREPARED: - playbackService.play(); + playbackService.resume(); break; case PREPARING: playbackService.setStartWhenPrepared(!playbackService @@ -609,7 +614,7 @@ public abstract class PlaybackController { public int getPosition() { if (playbackService != null) { - return playbackService.getCurrentPositionSafe(); + return playbackService.getCurrentPosition(); } else { return PlaybackService.INVALID_TIME; } @@ -617,7 +622,7 @@ public abstract class PlaybackController { public int getDuration() { if (playbackService != null) { - return playbackService.getDurationSafe(); + return playbackService.getDuration(); } else { return PlaybackService.INVALID_TIME; } @@ -675,27 +680,35 @@ public abstract class PlaybackController { return playbackService != null && playbackService.canSetSpeed(); } - public void setPlaybackSpeed(float speed) { - if (playbackService != null) { - playbackService.setSpeed(speed); - } - } - - public float getCurrentPlaybackSpeedMultiplier() { - if (canSetPlaybackSpeed()) { - return playbackService.getCurrentPlaybackSpeed(); - } else { - return -1; - } - } + public void setPlaybackSpeed(float speed) { + if (playbackService != null) { + playbackService.setSpeed(speed); + } + } + + public float getCurrentPlaybackSpeedMultiplier() { + if (canSetPlaybackSpeed()) { + return playbackService.getCurrentPlaybackSpeed(); + } else { + return -1; + } + } public boolean isPlayingVideo() { if (playbackService != null) { - return PlaybackService.isPlayingVideo(); + return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; } return false; } + public Pair<Integer, Integer> getVideoSize() { + if (playbackService != null) { + return playbackService.getVideoSize(); + } else { + return null; + } + } + /** * Returns true if PlaybackController can communicate with the playback @@ -716,7 +729,7 @@ public abstract class PlaybackController { */ public void reinitServiceIfPaused() { if (playbackService != null - && playbackService.isShouldStream() + && playbackService.isStreaming() && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService .getStatus() == PlayerStatus.PREPARING && playbackService .isStartWhenPrepared() == false))) { @@ -733,8 +746,7 @@ public abstract class PlaybackController { @Override public void run() { - if (playbackService != null && playbackService.getPlayer() != null - && playbackService.getPlayer().isPlaying()) { + if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) { activity.runOnUiThread(new Runnable() { @Override diff --git a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java index f0a50542c..ea9c692ab 100644 --- a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java +++ b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java @@ -59,4 +59,9 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { Log.e(TAG, "Setting playback speed unsupported in video player"); throw new UnsupportedOperationException("Setting playback speed unsupported in video player"); } + + @Override + public void setVideoScalingMode(int mode) { + super.setVideoScalingMode(mode); + } } diff --git a/src/de/danoeh/antennapod/view/AspectRatioVideoView.java b/src/de/danoeh/antennapod/view/AspectRatioVideoView.java new file mode 100644 index 000000000..f930c912a --- /dev/null +++ b/src/de/danoeh/antennapod/view/AspectRatioVideoView.java @@ -0,0 +1,97 @@ +package de.danoeh.antennapod.view; + +/* + * Copyright (C) Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.VideoView; + +public class AspectRatioVideoView extends VideoView { + + + private int mVideoWidth; + private int mVideoHeight; + + public AspectRatioVideoView(Context context) { + this(context, null); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mVideoWidth = 0; + mVideoHeight = 0; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mVideoWidth <= 0 || mVideoHeight <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + float heightRatio = (float) mVideoHeight / (float) getHeight(); + float widthRatio = (float) mVideoWidth / (float) getWidth(); + + int scaledHeight; + int scaledWidth; + + if (heightRatio > widthRatio) { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / heightRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / heightRatio); + } else { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / widthRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / widthRatio); + } + + setMeasuredDimension(scaledWidth, scaledHeight); + } + + /** + * Source code originally from: + * http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625 + * + * @param videoWidth + * @param videoHeight + */ + public void setVideoSize(int videoWidth, int videoHeight) { + // Set the new video size + mVideoWidth = videoWidth; + mVideoHeight = videoHeight; + + /** + * If this isn't set the video is stretched across the + * SurfaceHolders display surface (i.e. the SurfaceHolder + * as the same size and the video is drawn to fit this + * display area). We want the size to be the video size + * and allow the aspectratio to handle how the surface is shown + */ + getHolder().setFixedSize(videoWidth, videoHeight); + + requestLayout(); + invalidate(); + } + +} diff --git a/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java b/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java index 8df35ce67..5506a3bc9 100644 --- a/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java +++ b/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java @@ -108,6 +108,12 @@ public class HttpDownloaderTest extends InstrumentationTestCase { assertFalse(new File(feedFile.getFile_url()).exists()); } + /* TODO: replace with smaller test file + public void testUrlWithSpaces() { + download("http://acedl.noxsolutions.com/ace/Don't Call Salman Rushdie Sneezy in Finland.mp3", "testUrlWithSpaces", true); + } + */ + private static class FeedFileImpl extends FeedFile { public FeedFileImpl(String download_url) { super(null, download_url, false); diff --git a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java new file mode 100644 index 000000000..8a270715f --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java @@ -0,0 +1,1169 @@ +package instrumentationTest.de.test.antennapod.service.playback; + +import android.content.Context; +import android.media.RemoteControlClient; +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.service.playback.PlayerStatus; +import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.playback.Playable; +import junit.framework.AssertionFailedError; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test class for PlaybackServiceMediaPlayer + */ +public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase { + private static final String TAG = "PlaybackServiceMediaPlayerTest"; + + private static final String PLAYABLE_FILE_URL = "http://hpr.dogphilosophy.net/test/mp3.mp3"; + private static final String PLAYABLE_DEST_URL = "psmptestfile.wav"; + private String PLAYABLE_LOCAL_URL = null; + private static final int LATCH_TIMEOUT_SECONDS = 10; + + private volatile AssertionFailedError assertionError; + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + assertionError = null; + + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + File cacheDir = context.getExternalFilesDir("testFiles"); + if (cacheDir == null) + cacheDir = context.getExternalFilesDir("testFiles"); + File dest = new File(cacheDir, PLAYABLE_DEST_URL); + + assertNotNull(cacheDir); + assertTrue(cacheDir.canWrite()); + assertTrue(cacheDir.canRead()); + if (!dest.exists()) { + InputStream i = new URL(PLAYABLE_FILE_URL).openStream(); + OutputStream o = new FileOutputStream(new File(cacheDir, PLAYABLE_DEST_URL)); + IOUtils.copy(i, o); + o.flush(); + o.close(); + i.close(); + } + PLAYABLE_LOCAL_URL = "file://" + dest.getAbsolutePath(); + } + + private void checkPSMPInfo(PlaybackServiceMediaPlayer.PSMPInfo info) { + try { + switch (info.playerStatus) { + case PLAYING: + case PAUSED: + case PREPARED: + case PREPARING: + case INITIALIZED: + case INITIALIZING: + case SEEKING: + assertNotNull(info.playable); + break; + case STOPPED: + assertNull(info.playable); + break; + case ERROR: + assertNull(info.playable); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + public void testInit() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, defaultCallback); + psmp.shutdown(); + } + + private Playable writeTestPlayable(String downloadUrl, String fileUrl) { + final Context c = getInstrumentation().getTargetContext(); + Feed f = new Feed(0, new Date(), "f", "l", "d", null, null, null, null, "i", null, null, "l", false); + f.setItems(new ArrayList<FeedItem>()); + FeedItem i = new FeedItem(0, "t", "i", "l", new Date(), false, f); + f.getItems().add(i); + FeedMedia media = new FeedMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0); + i.setMedia(media); + PodDBAdapter adapter = new PodDBAdapter(c); + adapter.open(); + adapter.setCompleteFeed(f); + assertTrue(media.getId() != 0); + adapter.close(); + return media; + } + + + public void testPlayMediaObjectStreamNoStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, false, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertFalse(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, true, false); + + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertTrue(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamNoStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(4); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, false, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED); + + psmp.shutdown(); + } + + public void testPlayMediaObjectStreamStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(5); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + + } else if (countDownLatch.getCount() == 5) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null); + psmp.playMediaObject(p, true, true, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalNoStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, false, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertFalse(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalStartNoPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(2); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + countDownLatch.countDown(); + } else { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + countDownLatch.countDown(); + } + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, true, false); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.INITIALIZED); + assertTrue(psmp.isStartWhenPrepared()); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalNoStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(4); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } + countDownLatch.countDown(); + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, false, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PREPARED); + psmp.shutdown(); + } + + public void testPlayMediaObjectLocalStartPrepare() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final CountDownLatch countDownLatch = new CountDownLatch(5); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + try { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) + throw new IllegalStateException("MediaPlayer error"); + if (countDownLatch.getCount() == 0) { + fail(); + } else if (countDownLatch.getCount() == 5) { + assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 4) { + assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 3) { + assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 2) { + assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus); + } else if (countDownLatch.getCount() == 1) { + assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus); + } + + } catch (AssertionFailedError e) { + if (assertionError == null) + assertionError = e; + } finally { + countDownLatch.countDown(); + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + psmp.playMediaObject(p, false, true, true); + boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + assertTrue(psmp.getPSMPInfo().playerStatus == PlayerStatus.PLAYING); + psmp.shutdown(); + } + + + private final PlaybackServiceMediaPlayer.PSMPCallback defaultCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + + private void pauseTestSkeleton(final PlayerStatus initialState, final boolean stream, final boolean abandonAudioFocus, final boolean reinit, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = (stream && reinit) ? 2 : 1; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else if (initialState != PlayerStatus.PLAYING) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + switch (newInfo.playerStatus) { + case PAUSED: + if (latchCount == countDownLatch.getCount()) + countDownLatch.countDown(); + else { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } + break; + case INITIALIZED: + if (stream && reinit && countDownLatch.getCount() < latchCount) { + countDownLatch.countDown(); + } else if (countDownLatch.getCount() < latchCount) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } + break; + } + } + + } + + @Override + public void shouldStop() { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to shouldStop"); + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + if (initialState == PlayerStatus.PLAYING) { + psmp.playMediaObject(p, stream, true, true); + } + psmp.pause(abandonAudioFocus, reinit); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res || initialState != PlayerStatus.PLAYING); + psmp.shutdown(); + } + + public void testPauseDefaultState() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.STOPPED, false, false, false, 1); + } + + public void testPausePlayingStateNoAbandonNoReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, false, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonNoReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, false, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonNoReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, true, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonNoReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, true, false, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, false, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateNoAbandonReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, false, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonReinitNoStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, false, true, true, LATCH_TIMEOUT_SECONDS); + } + + public void testPausePlayingStateAbandonReinitStream() throws InterruptedException { + pauseTestSkeleton(PlayerStatus.PLAYING, true, true, true, LATCH_TIMEOUT_SECONDS); + } + + private void resumeTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = (initialState == PlayerStatus.PAUSED || initialState == PlayerStatus.PLAYING) ? 2 : + (initialState == PlayerStatus.PREPARED) ? 1 : 0; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else if (newInfo.playerStatus == PlayerStatus.PLAYING) { + if (countDownLatch.getCount() == 0) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + countDownLatch.countDown(); + } + } + + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) { + assertionError = new AssertionFailedError("Unexpected call of onMediaPlayerError"); + } + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) { + boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED); + psmp.playMediaObject(writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL), false, startWhenPrepared, true); + } + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.resume(); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res || (initialState != PlayerStatus.PAUSED && initialState != PlayerStatus.PREPARED)); + psmp.shutdown(); + } + + public void testResumePausedState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS); + } + + public void testResumePreparedState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS); + } + + public void testResumePlayingState() throws InterruptedException { + resumeTestSkeleton(PlayerStatus.PLAYING, 1); + } + + private void prepareTestSkeleton(final PlayerStatus initialState, long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = 1; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + if (initialState == PlayerStatus.INITIALIZED && newInfo.playerStatus == PlayerStatus.PREPARED) { + countDownLatch.countDown(); + } else if (initialState != PlayerStatus.INITIALIZED && initialState == newInfo.playerStatus) { + countDownLatch.countDown(); + } + } + + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + if (initialState == PlayerStatus.INITIALIZED + || initialState == PlayerStatus.PLAYING + || initialState == PlayerStatus.PREPARED + || initialState == PlayerStatus.PAUSED) { + boolean prepareImmediately = (initialState != PlayerStatus.INITIALIZED); + boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED); + psmp.playMediaObject(p, false, startWhenPrepared, prepareImmediately); + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.prepare(); + } + + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (initialState != PlayerStatus.INITIALIZED) { + assertEquals(initialState, psmp.getPSMPInfo().playerStatus); + } + + if (assertionError != null) + throw assertionError; + assertTrue(res); + psmp.shutdown(); + } + + public void testPrepareInitializedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS); + } + + public void testPreparePlayingState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PLAYING, 1); + } + + public void testPreparePausedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PAUSED, 1); + } + + public void testPreparePreparedState() throws InterruptedException { + prepareTestSkeleton(PlayerStatus.PREPARED, 1); + } + + private void reinitTestSkeleton(final PlayerStatus initialState, final long timeoutSeconds) throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int latchCount = 2; + final CountDownLatch countDownLatch = new CountDownLatch(latchCount); + PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + checkPSMPInfo(newInfo); + if (newInfo.playerStatus == PlayerStatus.ERROR) { + if (assertionError == null) + assertionError = new UnexpectedStateChange(newInfo.playerStatus); + } else { + if (newInfo.playerStatus == initialState) { + countDownLatch.countDown(); + } else if (countDownLatch.getCount() < latchCount && newInfo.playerStatus == PlayerStatus.INITIALIZED) { + countDownLatch.countDown(); + } + } + } + + @Override + public void shouldStop() { + + } + + @Override + public void playbackSpeedChanged(float s) { + + } + + @Override + public void onBufferingUpdate(int percent) { + + } + + @Override + public boolean onMediaPlayerInfo(int code) { + return false; + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + if (assertionError == null) + assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError"); + return false; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + return false; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return null; + } + }; + PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback); + Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL); + boolean prepareImmediately = initialState != PlayerStatus.INITIALIZED; + boolean startImmediately = initialState != PlayerStatus.PREPARED; + psmp.playMediaObject(p, false, startImmediately, prepareImmediately); + if (initialState == PlayerStatus.PAUSED) { + psmp.pause(false, false); + } + psmp.reinit(); + boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS); + if (assertionError != null) + throw assertionError; + assertTrue(res); + psmp.shutdown(); + } + + public void testReinitPlayingState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PLAYING, LATCH_TIMEOUT_SECONDS); + } + + public void testReinitPausedState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS); + } + + public void testPreparedPlayingState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS); + } + + public void testReinitInitializedState() throws InterruptedException { + reinitTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS); + } + + private static class UnexpectedStateChange extends AssertionFailedError { + public UnexpectedStateChange(PlayerStatus status) { + super("Unexpected state change: " + status); + } + } +} diff --git a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java new file mode 100644 index 000000000..19f64b4cf --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java @@ -0,0 +1,333 @@ +package instrumentationTest.de.test.antennapod.service.playback; + +import android.content.Context; +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.feed.EventDistributor; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.service.playback.PlaybackServiceTaskManager; +import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.playback.Playable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test class for PlaybackServiceTaskManager + */ +public class PlaybackServiceTaskManagerTest extends InstrumentationTestCase { + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + assertTrue(PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext())); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + } + + public void testInit() { + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(getInstrumentation().getTargetContext(), defaultPSTM); + pstm.shutdown(); + } + + private List<FeedItem> writeTestQueue(String pref) { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + Feed f = new Feed(0, new Date(), "title", "link", "d", null, null, null, null, "id", null, "null", "url", false); + f.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + f.getItems().add(new FeedItem(0, pref + i, pref + i, "link", new Date(), true, f)); + } + PodDBAdapter adapter = new PodDBAdapter(c); + adapter.open(); + adapter.setCompleteFeed(f); + adapter.setQueue(f.getItems()); + adapter.close(); + + for (FeedItem item : f.getItems()) { + assertTrue(item.getId() != 0); + } + return f.getItems(); + } + + public void testGetQueueWriteBeforeCreation() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + List<FeedItem> queue = writeTestQueue("a"); + assertNotNull(queue); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + List<FeedItem> testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(queue.size() == testQueue.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(queue.get(i).getId() == testQueue.get(i).getId()); + } + pstm.shutdown(); + } + + public void testGetQueueWriteAfterCreation() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + List<FeedItem> testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(testQueue.isEmpty()); + + + final CountDownLatch countDownLatch = new CountDownLatch(1); + EventDistributor.EventListener queueListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + countDownLatch.countDown(); + } + }; + EventDistributor.getInstance().register(queueListener); + List<FeedItem> queue = writeTestQueue("a"); + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + countDownLatch.await(5000, TimeUnit.MILLISECONDS); + + assertNotNull(queue); + testQueue = pstm.getQueue(); + assertNotNull(testQueue); + assertTrue(queue.size() == testQueue.size()); + for (int i = 0; i < queue.size(); i++) { + assertTrue(queue.get(i).getId() == testQueue.get(i).getId()); + } + pstm.shutdown(); + } + + public void testStartPositionSaver() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_COUNTDOWNS = 2; + final int TIMEOUT = 3 * PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL; + final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + countDownLatch.countDown(); + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.startPositionSaver(); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testIsPositionSaverActive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startPositionSaver(); + assertTrue(pstm.isPositionSaverActive()); + pstm.shutdown(); + } + + public void testCancelPositionSaver() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startPositionSaver(); + pstm.cancelPositionSaver(); + assertFalse(pstm.isPositionSaverActive()); + pstm.shutdown(); + } + + public void testStartWidgetUpdater() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final int NUM_COUNTDOWNS = 2; + final int TIMEOUT = 3 * PlaybackServiceTaskManager.WIDGET_UPDATER_NOTIFICATION_INTERVAL; + final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + countDownLatch.countDown(); + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.startWidgetUpdater(); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testIsWidgetUpdaterActive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + assertTrue(pstm.isWidgetUpdaterActive()); + pstm.shutdown(); + } + + public void testCancelWidgetUpdater() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + pstm.cancelWidgetUpdater(); + assertFalse(pstm.isWidgetUpdaterActive()); + pstm.shutdown(); + } + + public void testCancelAllTasksNoTasksStarted() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.cancelAllTasks(); + assertFalse(pstm.isPositionSaverActive()); + assertFalse(pstm.isWidgetUpdaterActive()); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testCancelAllTasksAllTasksStarted() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.startWidgetUpdater(); + pstm.startPositionSaver(); + pstm.setSleepTimer(100000); + pstm.cancelAllTasks(); + assertFalse(pstm.isPositionSaverActive()); + assertFalse(pstm.isWidgetUpdaterActive()); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testSetSleepTimer() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final long TIME = 2000; + final long TIMEOUT = 2 * TIME; + final CountDownLatch countDownLatch = new CountDownLatch(1); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + if (countDownLatch.getCount() == 0) { + fail(); + } + countDownLatch.countDown(); + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.setSleepTimer(TIME); + countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + pstm.shutdown(); + } + + public void testDisableSleepTimer() throws InterruptedException { + final Context c = getInstrumentation().getTargetContext(); + final long TIME = 1000; + final long TIMEOUT = 2 * TIME; + final CountDownLatch countDownLatch = new CountDownLatch(1); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + fail("Sleeptimer expired"); + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }); + pstm.setSleepTimer(TIME); + pstm.disableSleepTimer(); + assertFalse(countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)); + pstm.shutdown(); + } + + public void testIsSleepTimerActivePositive() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.setSleepTimer(10000); + assertTrue(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + public void testIsSleepTimerActiveNegative() { + final Context c = getInstrumentation().getTargetContext(); + PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM); + pstm.setSleepTimer(10000); + pstm.disableSleepTimer(); + assertFalse(pstm.isSleepTimerActive()); + pstm.shutdown(); + } + + private final PlaybackServiceTaskManager.PSTMCallback defaultPSTM = new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + + } + + @Override + public void onSleepTimerExpired() { + + } + + @Override + public void onWidgetUpdaterTick() { + + } + + @Override + public void onChapterLoaded(Playable media) { + + } + }; +} diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java index 91ac61867..b03d83d25 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java @@ -4,11 +4,19 @@ import android.content.Context; import android.test.InstrumentationTestCase; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.FeedItemStatistics; import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import static instrumentationTest.de.test.antennapod.storage.DBTestUtils.*; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + +import static instrumentationTest.de.test.antennapod.storage.DBTestUtils.saveFeedlist; /** * Test class for DBReader @@ -36,7 +44,7 @@ public class DBReaderTest extends InstrumentationTestCase { private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) { final Context context = getInstrumentation().getTargetContext(); Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, - null, null, null, "feed", null, null, "url", false); + null, null, null, "feed", null, null, "url", false, new FlattrStatus()); feed.setItems(new ArrayList<FeedItem>()); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); @@ -65,7 +73,6 @@ public class DBReaderTest extends InstrumentationTestCase { } - public void testGetFeedList() { final Context context = getInstrumentation().getTargetContext(); List<Feed> feeds = saveFeedlist(context, 10, 0, false); @@ -77,6 +84,36 @@ public class DBReaderTest extends InstrumentationTestCase { } } + public void testGetFeedListSortOrder() { + final Context context = getInstrumentation().getTargetContext(); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Feed feed1 = new Feed(0, new Date(), "A", "link", "d", null, null, null, "rss", "A", null, "", "", true); + Feed feed2 = new Feed(0, new Date(), "b", "link", "d", null, null, null, "rss", "b", null, "", "", true); + Feed feed3 = new Feed(0, new Date(), "C", "link", "d", null, null, null, "rss", "C", null, "", "", true); + Feed feed4 = new Feed(0, new Date(), "d", "link", "d", null, null, null, "rss", "d", null, "", "", true); + adapter.setCompleteFeed(feed1); + adapter.setCompleteFeed(feed2); + adapter.setCompleteFeed(feed3); + adapter.setCompleteFeed(feed4); + assertTrue(feed1.getId() != 0); + assertTrue(feed2.getId() != 0); + assertTrue(feed3.getId() != 0); + assertTrue(feed4.getId() != 0); + + adapter.close(); + + List<Feed> saved = DBReader.getFeedList(context); + assertNotNull(saved); + assertEquals("Wrong size: ", 4, saved.size()); + + assertEquals("Wrong id of feed 1: ", feed1.getId(), saved.get(0).getId()); + assertEquals("Wrong id of feed 2: ", feed2.getId(), saved.get(1).getId()); + assertEquals("Wrong id of feed 3: ", feed3.getId(), saved.get(2).getId()); + assertEquals("Wrong id of feed 4: ", feed4.getId(), saved.get(3).getId()); + } + public void testFeedListDownloadUrls() { final Context context = getInstrumentation().getTargetContext(); List<Feed> feeds = saveFeedlist(context, 10, 0, false); @@ -286,4 +323,46 @@ public class DBReaderTest extends InstrumentationTestCase { assertTrue(found); } } + + public void testGetPlaybackHistory() { + final Context context = getInstrumentation().getTargetContext(); + final int numItems = 10; + final int playedItems = 5; + final int numFeeds = 1; + + Feed feed = DBTestUtils.saveFeedlist(context, numFeeds, numItems, true).get(0); + long[] ids = new long[playedItems]; + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (int i = 0; i < playedItems; i++) { + FeedMedia m = feed.getItems().get(i).getMedia(); + m.setPlaybackCompletionDate(new Date(i + 1)); + adapter.setFeedMediaPlaybackCompletionDate(m); + ids[ids.length - 1 - i] = m.getItem().getId(); + } + adapter.close(); + + List<FeedItem> saved = DBReader.getPlaybackHistory(context); + assertNotNull(saved); + assertEquals("Wrong size: ", playedItems, saved.size()); + for (int i = 0; i < playedItems; i++) { + FeedItem item = saved.get(i); + assertNotNull(item.getMedia().getPlaybackCompletionDate()); + assertEquals("Wrong sort order: ", item.getId(), ids[i]); + } + } + + public void testGetFeedStatisticsCheckOrder() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_FEEDS = 10; + final int NUM_ITEMS = 10; + List<Feed> feeds = DBTestUtils.saveFeedlist(context, NUM_FEEDS, NUM_ITEMS, false); + List<FeedItemStatistics> statistics = DBReader.getFeedStatisticsList(context); + assertNotNull(statistics); + assertEquals(feeds.size(), statistics.size()); + for (int i = 0; i < NUM_FEEDS; i++) { + assertEquals("Wrong entry at index " + i, feeds.get(i).getId(), statistics.get(i).getFeedID()); + } + } } diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java index e9b871867..2372757ce 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java @@ -11,6 +11,7 @@ import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import java.io.File; import java.io.IOException; @@ -76,7 +77,7 @@ public class DBTasksTest extends InstrumentationTestCase { File f = new File(destFolder, "file " + i); assertTrue(f.createNewFile()); files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i))); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); items.add(item); } @@ -114,7 +115,7 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(f.createNewFile()); assertTrue(f.exists()); files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i))); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); items.add(item); } @@ -148,7 +149,7 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(f.createNewFile()); assertTrue(f.exists()); files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i))); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, new Date(NUM_ITEMS - i), 0)); items.add(item); } @@ -279,7 +280,7 @@ public class DBTasksTest extends InstrumentationTestCase { final Context context = getInstrumentation().getTargetContext(); UserPreferences.setUpdateInterval(context, expirationTime); Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null, - null, null, null, "feed", null, null, "url", false); + null, null, null, "feed", null, null, "url", false, new FlattrStatus()); feed.setItems(new ArrayList<FeedItem>()); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java b/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java index fbb7b0386..7e9e1b908 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java @@ -6,6 +6,7 @@ import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.PodDBAdapter; import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.util.flattr.FlattrStatus; import junit.framework.Assert; import java.util.ArrayList; @@ -31,7 +32,7 @@ public class DBTestUtils { adapter.open(); for (int i = 0; i < numFeeds; i++) { Feed f = new Feed(0, new Date(), "feed " + i, "link" + i, "descr", null, null, - null, null, "id" + i, null, null, "url" + i, false); + null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus()); f.setItems(new ArrayList<FeedItem>()); for (int j = 0; j < numItems; j++) { FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(), diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java index 429903cba..679ae1ad3 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java @@ -64,7 +64,7 @@ public class DBWriterTest extends InstrumentationTestCase { feed.setItems(items); FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), true, feed); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null, 0); item.setMedia(media); items.add(item); @@ -108,7 +108,7 @@ public class DBWriterTest extends InstrumentationTestCase { assertTrue(enc.createNewFile()); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0); item.setMedia(media); } @@ -169,7 +169,7 @@ public class DBWriterTest extends InstrumentationTestCase { assertTrue(enc.createNewFile()); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0); item.setMedia(media); } @@ -317,7 +317,7 @@ public class DBWriterTest extends InstrumentationTestCase { File enc = new File(destFolder, "file " + i); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0); item.setMedia(media); } @@ -389,7 +389,7 @@ public class DBWriterTest extends InstrumentationTestCase { File enc = new File(destFolder, "file " + i); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0); item.setMedia(media); } @@ -430,7 +430,7 @@ public class DBWriterTest extends InstrumentationTestCase { Feed feed = new Feed("url", new Date(), "title"); feed.setItems(new ArrayList<FeedItem>()); FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed); - FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, "url", false, playbackCompletionDate); + FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, "url", false, playbackCompletionDate, 0); feed.getItems().add(item); item.setMedia(media); PodDBAdapter adapter = new PodDBAdapter(context); diff --git a/submodules/dslv b/submodules/dslv -Subproject f294d8eec59f73b5594634cac6fe1dc4e2cb32b +Subproject 1bb29afb91f41af53f136599cf349752c13f52a |