summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
authordaniel oeh <daniel.oeh@gmail.com>2014-10-24 20:40:07 +0200
committerdaniel oeh <daniel.oeh@gmail.com>2014-10-24 20:40:07 +0200
commitcc052e91ad8a87b00b93649ec0f6a06bcae6267a (patch)
tree12cacac4fb5c94af2955812a3167eefb325f286d /app/src
parentbaa7d5f11283cb7668d45b561af5d38f0ccb9632 (diff)
parentb5066d02b4acf31da093190a1a57a9d961bb04ca (diff)
downloadAntennaPod-cc052e91ad8a87b00b93649ec0f6a06bcae6267a.zip
Merge branch 'migration' into develop
Non-GUI classes have been moved into the 'core' project in order to allow AntennaPod SP to reference it as a subproject. Conflicts: app/src/main/AndroidManifest.xml build.gradle core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java gradle/wrapper/gradle-wrapper.properties pom.xml
Diffstat (limited to 'app/src')
-rw-r--r--app/src/androidTest/assets/testfile.mp3bin0 -> 20606 bytes
-rw-r--r--app/src/androidTest/java/de/test/antennapod/AntennaPodTestRunner.java16
-rw-r--r--app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java114
-rw-r--r--app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java176
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java170
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java1177
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java333
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java407
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java326
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java57
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java796
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java115
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java149
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java200
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java94
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java38
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java35
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java59
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java21
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java76
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java127
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java346
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/service/download/NanoHTTPD.java1420
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java109
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java118
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java28
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java21
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java110
-rw-r--r--app/src/main/AndroidManifest.xml317
-rw-r--r--app/src/main/assets/LICENSE.html17
-rw-r--r--app/src/main/assets/LICENSE_APACHE_COMMONS.txt202
-rw-r--r--app/src/main/assets/LICENSE_BETTERPICKERS.txt13
-rw-r--r--app/src/main/assets/LICENSE_DSLV.txt16
-rw-r--r--app/src/main/assets/LICENSE_FLATTR4J.txt202
-rw-r--r--app/src/main/assets/LICENSE_JSOUP.txt21
-rw-r--r--app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt202
-rw-r--r--app/src/main/assets/LICENSE_OKHTTP.txt11
-rw-r--r--app/src/main/assets/LICENSE_OKIO.txt202
-rw-r--r--app/src/main/assets/LICENSE_PICASSO.txt13
-rw-r--r--app/src/main/assets/LICENSE_PRESTO.txt13
-rw-r--r--app/src/main/assets/Roboto-Light.ttfbin0 -> 115200 bytes
-rw-r--r--app/src/main/assets/Roboto.ttfbin0 -> 114976 bytes
-rw-r--r--app/src/main/assets/about.html83
-rwxr-xr-xapp/src/main/assets/logo.pngbin0 -> 58799 bytes
-rw-r--r--app/src/main/assets/testfile.mp3bin0 -> 20606 bytes
-rw-r--r--app/src/main/java/de/danoeh/antennapod/AppConfig.java7
-rw-r--r--app/src/main/java/de/danoeh/antennapod/PodcastApp.java55
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java32
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java746
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java248
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java370
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java110
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java192
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java125
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java432
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java522
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java428
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java134
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java90
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java38
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java171
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java29
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java532
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java75
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java359
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java372
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java8
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java78
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java57
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java180
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java57
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java112
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java122
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java142
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java306
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java219
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java55
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java229
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java170
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java127
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java110
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java68
-rw-r--r--app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java118
-rw-r--r--app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java69
-rw-r--r--app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java116
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java29
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java19
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java60
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java53
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java22
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java32
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java107
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java89
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java107
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java434
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java67
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java138
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java100
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java76
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java196
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java105
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java121
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java145
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java238
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java476
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java456
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java425
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java288
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java383
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java69
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java258
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java131
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java167
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java20
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java80
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java26
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java50
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java146
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java191
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java86
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java31
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java9
-rw-r--r--app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java46
-rw-r--r--app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java49
-rw-r--r--app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java55
-rw-r--r--app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java203
-rw-r--r--app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java69
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java97
-rw-r--r--app/src/main/res/anim/fade_in.xml9
-rw-r--r--app/src/main/res/anim/fade_out.xml10
-rw-r--r--app/src/main/res/layout-land/audioplayer_activity.xml197
-rw-r--r--app/src/main/res/layout-land/videoplayer_activity.xml84
-rw-r--r--app/src/main/res/layout-v14/authentication_dialog.xml81
-rw-r--r--app/src/main/res/layout-v14/directory_chooser.xml107
-rw-r--r--app/src/main/res/layout-v14/download_authentication_activity.xml92
-rw-r--r--app/src/main/res/layout-v14/opml_selection.xml61
-rw-r--r--app/src/main/res/layout-v14/time_dialog.xml78
-rw-r--r--app/src/main/res/layout/about.xml12
-rw-r--r--app/src/main/res/layout/addfeed.xml100
-rw-r--r--app/src/main/res/layout/audioplayer_activity.xml182
-rw-r--r--app/src/main/res/layout/authentication_dialog.xml62
-rw-r--r--app/src/main/res/layout/autoflattr_preference_dialog.xml35
-rw-r--r--app/src/main/res/layout/cover_fragment.xml19
-rw-r--r--app/src/main/res/layout/directory_chooser.xml85
-rw-r--r--app/src/main/res/layout/download_authentication_activity.xml69
-rw-r--r--app/src/main/res/layout/downloaded_episodeslist_item.xml82
-rw-r--r--app/src/main/res/layout/downloadlist_item.xml89
-rw-r--r--app/src/main/res/layout/downloadlog_item.xml71
-rw-r--r--app/src/main/res/layout/ellipsize_start_listitem.xml19
-rw-r--r--app/src/main/res/layout/external_itemlist_item.xml115
-rw-r--r--app/src/main/res/layout/external_player_fragment.xml59
-rw-r--r--app/src/main/res/layout/feedinfo.xml204
-rw-r--r--app/src/main/res/layout/feeditem_dialog.xml72
-rw-r--r--app/src/main/res/layout/feeditemlist_header.xml65
-rw-r--r--app/src/main/res/layout/feeditemlist_item.xml101
-rw-r--r--app/src/main/res/layout/flattr_auth.xml30
-rw-r--r--app/src/main/res/layout/gpodnet_podcast_list.xml45
-rw-r--r--app/src/main/res/layout/gpodnet_podcast_listitem.xml45
-rw-r--r--app/src/main/res/layout/gpodnetauth_activity.xml10
-rw-r--r--app/src/main/res/layout/gpodnetauth_credentials.xml83
-rw-r--r--app/src/main/res/layout/gpodnetauth_device.xml114
-rw-r--r--app/src/main/res/layout/gpodnetauth_finish.xml42
-rw-r--r--app/src/main/res/layout/itemdescription_listitem.xml27
-rw-r--r--app/src/main/res/layout/listview_activity.xml12
-rw-r--r--app/src/main/res/layout/main.xml40
-rw-r--r--app/src/main/res/layout/nav_feedlistitem.xml39
-rw-r--r--app/src/main/res/layout/nav_listitem.xml53
-rw-r--r--app/src/main/res/layout/nav_section_item.xml26
-rw-r--r--app/src/main/res/layout/new_episodes_fragment.xml43
-rw-r--r--app/src/main/res/layout/new_episodes_listitem.xml111
-rw-r--r--app/src/main/res/layout/onlinefeedview_header.xml83
-rw-r--r--app/src/main/res/layout/opml_import.xml27
-rw-r--r--app/src/main/res/layout/opml_selection.xml39
-rw-r--r--app/src/main/res/layout/pager_fragment.xml12
-rw-r--r--app/src/main/res/layout/player_widget.xml52
-rw-r--r--app/src/main/res/layout/queue_fragment.xml42
-rw-r--r--app/src/main/res/layout/queue_listitem.xml96
-rw-r--r--app/src/main/res/layout/searchlist_item.xml43
-rw-r--r--app/src/main/res/layout/simplechapter_item.xml43
-rw-r--r--app/src/main/res/layout/storage_error.xml25
-rw-r--r--app/src/main/res/layout/time_dialog.xml54
-rw-r--r--app/src/main/res/menu/directory_chooser.xml14
-rw-r--r--app/src/main/res/menu/feedinfo.xml28
-rw-r--r--app/src/main/res/menu/feeditem.xml77
-rw-r--r--app/src/main/res/menu/feeditem_dialog.xml48
-rw-r--r--app/src/main/res/menu/feedlist.xml34
-rw-r--r--app/src/main/res/menu/main.xml13
-rw-r--r--app/src/main/res/menu/mediaplayer.xml40
-rw-r--r--app/src/main/res/menu/new_episodes.xml27
-rw-r--r--app/src/main/res/menu/queue_context.xml20
-rw-r--r--app/src/main/res/xml/player_widget_info.xml4
-rw-r--r--app/src/main/res/xml/preferences.xml157
-rw-r--r--app/src/main/res/xml/searchable.xml4
193 files changed, 25171 insertions, 0 deletions
diff --git a/app/src/androidTest/assets/testfile.mp3 b/app/src/androidTest/assets/testfile.mp3
new file mode 100644
index 000000000..f15faadf3
--- /dev/null
+++ b/app/src/androidTest/assets/testfile.mp3
Binary files differ
diff --git a/app/src/androidTest/java/de/test/antennapod/AntennaPodTestRunner.java b/app/src/androidTest/java/de/test/antennapod/AntennaPodTestRunner.java
new file mode 100644
index 000000000..24cd6e669
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/AntennaPodTestRunner.java
@@ -0,0 +1,16 @@
+package de.test.antennapod;
+
+import android.test.InstrumentationTestRunner;
+import android.test.suitebuilder.TestSuiteBuilder;
+import junit.framework.TestSuite;
+
+public class AntennaPodTestRunner extends InstrumentationTestRunner {
+
+ @Override
+ public TestSuite getAllTests() {
+ return new TestSuiteBuilder(AntennaPodTestRunner.class)
+ .includeAllPackagesUnderHere()
+ .excludePackages("de.test.antennapod.gpodnet")
+ .build();
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java
new file mode 100644
index 000000000..14a3b27b0
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java
@@ -0,0 +1,114 @@
+package de.test.antennapod.gpodnet;
+
+import android.test.AndroidTestCase;
+
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test class for GpodnetService
+ */
+public class GPodnetServiceTest extends AndroidTestCase {
+
+ private GpodnetService service;
+
+ private static final String USER = "";
+ private static final String PW = "";
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ service = new GpodnetService();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private void authenticate() throws GpodnetServiceException {
+ service.authenticate(USER, PW);
+ }
+
+ public void testUploadSubscription() throws GpodnetServiceException {
+ authenticate();
+ ArrayList<String> l = new ArrayList<String>();
+ l.add("http://bitsundso.de/feed");
+ service.uploadSubscriptions(USER, "radio", l);
+ }
+
+ public void testUploadSubscription2() throws GpodnetServiceException {
+ authenticate();
+ ArrayList<String> l = new ArrayList<String>();
+ l.add("http://bitsundso.de/feed");
+ l.add("http://gamesundso.de/feed");
+ service.uploadSubscriptions(USER, "radio", l);
+ }
+
+ public void testUploadChanges() throws GpodnetServiceException {
+ authenticate();
+ String[] URLS = {"http://bitsundso.de/feed", "http://gamesundso.de/feed", "http://cre.fm/feed/mp3/", "http://freakshow.fm/feed/m4a/"};
+ List<String> subscriptions = Arrays.asList(URLS[0], URLS[1]);
+ List<String> removed = Arrays.asList(URLS[0]);
+ List<String> added = Arrays.asList(URLS[2], URLS[3]);
+ service.uploadSubscriptions(USER, "radio", subscriptions);
+ service.uploadChanges(USER, "radio", added, removed);
+ }
+
+ public void testGetSubscriptionChanges() throws GpodnetServiceException {
+ authenticate();
+ service.getSubscriptionChanges(USER, "radio", 1362322610L);
+ }
+
+ public void testGetSubscriptionsOfUser()
+ throws GpodnetServiceException {
+ authenticate();
+ service.getSubscriptionsOfUser(USER);
+ }
+
+ public void testGetSubscriptionsOfDevice()
+ throws GpodnetServiceException {
+ authenticate();
+ service.getSubscriptionsOfDevice(USER, "radio");
+ }
+
+ public void testConfigureDevices() throws GpodnetServiceException {
+ authenticate();
+ service.configureDevice(USER, "foo", "This is an updated caption",
+ GpodnetDevice.DeviceType.LAPTOP);
+ }
+
+ public void testGetDevices() throws GpodnetServiceException {
+ authenticate();
+ service.getDevices(USER);
+ }
+
+ public void testGetSuggestions() throws GpodnetServiceException {
+ authenticate();
+ service.getSuggestions(10);
+ }
+
+ public void testTags() throws GpodnetServiceException {
+ service.getTopTags(20);
+ }
+
+ public void testPodcastForTags() throws GpodnetServiceException {
+ List<GpodnetTag> tags = service.getTopTags(20);
+ service.getPodcastsForTag(tags.get(1),
+ 10);
+ }
+
+ public void testSearch() throws GpodnetServiceException {
+ service.searchPodcasts("linux", 64);
+ }
+
+ public void testToplist() throws GpodnetServiceException {
+ service.getPodcastToplist(10);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java b/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java
new file mode 100644
index 000000000..52067c971
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java
@@ -0,0 +1,176 @@
+package de.test.antennapod.handler;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.core.feed.*;
+import de.danoeh.antennapod.core.syndication.handler.FeedHandler;
+import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException;
+import de.test.antennapod.util.syndication.feedgenerator.AtomGenerator;
+import de.test.antennapod.util.syndication.feedgenerator.FeedGenerator;
+import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Tests for FeedHandler
+ */
+public class FeedHandlerTest extends InstrumentationTestCase {
+ private static final String FEEDS_DIR = "testfeeds";
+
+ File file = null;
+ OutputStream outputStream = null;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ Context context = getInstrumentation().getContext();
+ File destDir = context.getExternalFilesDir(FEEDS_DIR);
+ assertNotNull(destDir);
+
+ file = new File(destDir, "feed.xml");
+ file.delete();
+
+ assertNotNull(file);
+ assertFalse(file.exists());
+
+ outputStream = new FileOutputStream(file);
+ }
+
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ file.delete();
+ file = null;
+
+ outputStream.close();
+ outputStream = null;
+ }
+
+ private Feed runFeedTest(Feed feed, FeedGenerator g, String encoding, long flags) throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException {
+ g.writeFeed(feed, outputStream, encoding, flags);
+ FeedHandler handler = new FeedHandler();
+ Feed parsedFeed = new Feed(feed.getDownload_url(), feed.getLastUpdate());
+ parsedFeed.setFile_url(file.getAbsolutePath());
+ parsedFeed.setDownloaded(true);
+ handler.parseFeed(parsedFeed);
+ return parsedFeed;
+ }
+
+ private void feedValid(Feed feed, Feed parsedFeed, String feedType) {
+ assertEquals(feed.getTitle(), parsedFeed.getTitle());
+ if (feedType.equals(Feed.TYPE_ATOM1)) {
+ assertEquals(feed.getFeedIdentifier(), parsedFeed.getFeedIdentifier());
+ } else {
+ assertEquals(feed.getLanguage(), parsedFeed.getLanguage());
+ }
+
+ assertEquals(feed.getLink(), parsedFeed.getLink());
+ assertEquals(feed.getDescription(), parsedFeed.getDescription());
+ assertEquals(feed.getPaymentLink(), parsedFeed.getPaymentLink());
+
+ if (feed.getImage() != null) {
+ FeedImage image = feed.getImage();
+ FeedImage parsedImage = parsedFeed.getImage();
+ assertNotNull(parsedImage);
+
+ assertEquals(image.getTitle(), parsedImage.getTitle());
+ assertEquals(image.getDownload_url(), parsedImage.getDownload_url());
+ }
+
+ if (feed.getItems() != null) {
+ assertNotNull(parsedFeed.getItems());
+ assertEquals(feed.getItems().size(), parsedFeed.getItems().size());
+
+ for (int i = 0; i < feed.getItems().size(); i++) {
+ FeedItem item = feed.getItems().get(i);
+ FeedItem parsedItem = parsedFeed.getItems().get(i);
+
+ if (item.getItemIdentifier() != null)
+ assertEquals(item.getItemIdentifier(), parsedItem.getItemIdentifier());
+ assertEquals(item.getTitle(), parsedItem.getTitle());
+ assertEquals(item.getDescription(), parsedItem.getDescription());
+ assertEquals(item.getContentEncoded(), parsedItem.getContentEncoded());
+ assertEquals(item.getLink(), parsedItem.getLink());
+ assertEquals(item.getPubDate().getTime(), parsedItem.getPubDate().getTime());
+ assertEquals(item.getPaymentLink(), parsedItem.getPaymentLink());
+
+ if (item.hasMedia()) {
+ assertTrue(parsedItem.hasMedia());
+ FeedMedia media = item.getMedia();
+ FeedMedia parsedMedia = parsedItem.getMedia();
+
+ assertEquals(media.getDownload_url(), parsedMedia.getDownload_url());
+ assertEquals(media.getSize(), parsedMedia.getSize());
+ assertEquals(media.getMime_type(), parsedMedia.getMime_type());
+ }
+
+ if (item.hasItemImage()) {
+ assertTrue(parsedItem.hasItemImage());
+ FeedImage image = item.getImage();
+ FeedImage parsedImage = parsedItem.getImage();
+
+ assertEquals(image.getTitle(), parsedImage.getTitle());
+ assertEquals(image.getDownload_url(), parsedImage.getDownload_url());
+ }
+
+ if (item.getChapters() != null) {
+ assertNotNull(parsedItem.getChapters());
+ assertEquals(item.getChapters().size(), parsedItem.getChapters().size());
+ List<Chapter> chapters = item.getChapters();
+ List<Chapter> parsedChapters = parsedItem.getChapters();
+ for (int j = 0; j < chapters.size(); j++) {
+ Chapter chapter = chapters.get(j);
+ Chapter parsedChapter = parsedChapters.get(j);
+
+ assertEquals(chapter.getTitle(), parsedChapter.getTitle());
+ assertEquals(chapter.getLink(), parsedChapter.getLink());
+ }
+ }
+ }
+ }
+ }
+
+ public void testRSS2Basic() throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException {
+ Feed f1 = createTestFeed(10, false, true, true);
+ Feed f2 = runFeedTest(f1, new RSS2Generator(), "UTF-8", RSS2Generator.FEATURE_WRITE_GUID);
+ feedValid(f1, f2, Feed.TYPE_RSS2);
+ }
+
+ public void testAtomBasic() throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException {
+ Feed f1 = createTestFeed(10, false, true, true);
+ Feed f2 = runFeedTest(f1, new AtomGenerator(), "UTF-8", 0);
+ feedValid(f1, f2, Feed.TYPE_ATOM1);
+ }
+
+ private Feed createTestFeed(int numItems, boolean withImage, boolean withFeedMedia, boolean withChapters) {
+ FeedImage image = null;
+ if (withImage) {
+ image = new FeedImage(0, "image", null, "http://example.com/picture", false);
+ }
+ Feed feed = new Feed(0, new Date(), "title", "http://example.com", "This is the description",
+ "http://example.com/payment", "Daniel", "en", null, "http://example.com/feed", image, file.getAbsolutePath(),
+ "http://example.com/feed", true);
+ feed.setItems(new ArrayList<FeedItem>());
+
+ for (int i = 0; i < numItems; i++) {
+ FeedItem item = new FeedItem(0, "item-" + i, "http://example.com/item-" + i,
+ "http://example.com/items/" + i, new Date(i*60000), false, feed);
+ feed.getItems().add(item);
+ if (withFeedMedia) {
+ item.setMedia(new FeedMedia(0, item, 4711, 0, 100, "audio/mp3", null, "http://example.com/media-" + i,
+ false, null, 0));
+ }
+ }
+
+ return feed;
+ }
+
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java
new file mode 100644
index 000000000..1a561f282
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java
@@ -0,0 +1,170 @@
+package de.test.antennapod.service.download;
+
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+import de.danoeh.antennapod.core.feed.FeedFile;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.service.download.HttpDownloader;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.test.antennapod.util.service.download.HTTPBin;
+
+import java.io.File;
+import java.io.IOException;
+
+public class HttpDownloaderTest extends InstrumentationTestCase {
+ private static final String TAG = "HttpDownloaderTest";
+ private static final String DOWNLOAD_DIR = "testdownloads";
+
+ private static boolean successful = true;
+
+ private File destDir;
+
+ private HTTPBin httpServer;
+
+ public HttpDownloaderTest() {
+ super();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ File[] contents = destDir.listFiles();
+ for (File f : contents) {
+ assertTrue(f.delete());
+ }
+
+ httpServer.stop();
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ destDir = getInstrumentation().getTargetContext().getExternalFilesDir(DOWNLOAD_DIR);
+ assertNotNull(destDir);
+ assertTrue(destDir.exists());
+ httpServer = new HTTPBin();
+ httpServer.start();
+ }
+
+ private FeedFileImpl setupFeedFile(String downloadUrl, String title, boolean deleteExisting) {
+ FeedFileImpl feedfile = new FeedFileImpl(downloadUrl);
+ String fileUrl = new File(destDir, title).getAbsolutePath();
+ File file = new File(fileUrl);
+ if (deleteExisting) {
+ Log.d(TAG, "Deleting file: " + file.delete());
+ }
+ feedfile.setFile_url(fileUrl);
+ return feedfile;
+ }
+
+ private Downloader download(String url, String title, boolean expectedResult) {
+ return download(url, title, expectedResult, true, null, null, true);
+ }
+
+ private Downloader download(String url, String title, boolean expectedResult, boolean deleteExisting, String username, String password, boolean deleteOnFail) {
+ FeedFile feedFile = setupFeedFile(url, title, deleteExisting);
+ DownloadRequest request = new DownloadRequest(feedFile.getFile_url(), url, title, 0, feedFile.getTypeAsInt(), username, password, deleteOnFail);
+ Downloader downloader = new HttpDownloader(request);
+ downloader.call();
+ DownloadStatus status = downloader.getResult();
+ assertNotNull(status);
+ assertTrue(status.isSuccessful() == expectedResult);
+ assertTrue(status.isDone());
+ // the file should not exist if the download has failed and deleteExisting was true
+ assertTrue(!deleteExisting || new File(feedFile.getFile_url()).exists() == expectedResult);
+ return downloader;
+ }
+
+
+ private static final String URL_404 = HTTPBin.BASE_URL + "/status/404";
+ private static final String URL_AUTH = HTTPBin.BASE_URL + "/basic-auth/user/passwd";
+
+ public void testPassingHttp() {
+ download(HTTPBin.BASE_URL + "/status/200", "test200", true);
+ }
+
+ public void testRedirect() {
+ download(HTTPBin.BASE_URL + "/redirect/4", "testRedirect", true);
+ }
+
+ public void testGzip() {
+ download("http://httpbin.org/gzip", "testGzip", true);
+ }
+
+ public void test404() {
+ download(URL_404, "test404", false);
+ }
+
+ public void testCancel() {
+ final String url = HTTPBin.BASE_URL + "/delay/3";
+ FeedFileImpl feedFile = setupFeedFile(url, "delay", true);
+ final Downloader downloader = new HttpDownloader(new DownloadRequest(feedFile.getFile_url(), url, "delay", 0, feedFile.getTypeAsInt()));
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ downloader.call();
+ }
+ };
+ t.start();
+ downloader.cancel();
+ try {
+ t.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ DownloadStatus result = downloader.getResult();
+ assertTrue(result.isDone());
+ assertFalse(result.isSuccessful());
+ assertTrue(result.isCancelled());
+ assertFalse(new File(feedFile.getFile_url()).exists());
+ }
+
+ public void testDeleteOnFailShouldDelete() {
+ Downloader downloader = download(URL_404, "testDeleteOnFailShouldDelete", false, true, null, null, true);
+ assertFalse(new File(downloader.getDownloadRequest().getDestination()).exists());
+ }
+
+ public void testDeleteOnFailShouldNotDelete() throws IOException {
+ String filename = "testDeleteOnFailShouldDelete";
+ File dest = new File(destDir, filename);
+ dest.delete();
+ assertTrue(dest.createNewFile());
+ Downloader downloader = download(URL_404, filename, false, false, null, null, false);
+ assertTrue(new File(downloader.getDownloadRequest().getDestination()).exists());
+ }
+
+ public void testAuthenticationShouldSucceed() throws InterruptedException {
+ download(URL_AUTH, "testAuthSuccess", true, true, "user", "passwd", true);
+ }
+
+ public void testAuthenticationShouldFail() {
+ Downloader downloader = download(URL_AUTH, "testAuthSuccess", false, true, "user", "Wrong passwd", true);
+ assertEquals(DownloadError.ERROR_UNAUTHORIZED, downloader.getResult().getReason());
+ }
+
+ /* 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);
+ }
+
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ return download_url;
+ }
+
+ @Override
+ public int getTypeAsInt() {
+ return 0;
+ }
+ }
+
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
new file mode 100644
index 000000000..aac4c245a
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
@@ -0,0 +1,1177 @@
+package de.test.antennapod.service.playback;
+
+import android.content.Context;
+import android.media.RemoteControlClient;
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.test.antennapod.util.service.download.HTTPBin;
+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.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://127.0.0.1:" + HTTPBin.PORT + "/files/0";
+ private static final String PLAYABLE_DEST_URL = "psmptestfile.mp3";
+ private String PLAYABLE_LOCAL_URL = null;
+ private static final int LATCH_TIMEOUT_SECONDS = 10;
+
+ private HTTPBin httpServer;
+
+ private volatile AssertionFailedError assertionError;
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext());
+ httpServer.stop();
+ }
+
+ @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();
+
+ httpServer = new HTTPBin();
+ httpServer.start();
+
+ 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 = getInstrumentation().getContext().getAssets().open("testfile.mp3");
+ 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();
+ assertEquals(0, httpServer.serveFile(dest));
+ }
+
+ 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/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
new file mode 100644
index 000000000..81d684595
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
@@ -0,0 +1,333 @@
+package de.test.antennapod.service.playback;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.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/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java
new file mode 100644
index 000000000..63286d11d
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java
@@ -0,0 +1,407 @@
+package de.test.antennapod.storage;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.FeedItemStatistics;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+import static de.test.antennapod.storage.DBTestUtils.saveFeedlist;
+
+/**
+ * Test class for DBReader
+ */
+public class DBReaderTest extends InstrumentationTestCase {
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ final Context context = getInstrumentation().getTargetContext();
+ assertTrue(PodDBAdapter.deleteDatabase(context));
+ }
+
+ @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();
+ }
+
+ 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, new FlattrStatus());
+ feed.setItems(new ArrayList<FeedItem>());
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ List<Feed> expiredFeeds = DBReader.getExpiredFeedsList(context, expirationTime);
+ assertNotNull(expiredFeeds);
+ if (shouldReturn) {
+ assertTrue(expiredFeeds.size() == 1);
+ assertTrue(expiredFeeds.get(0).getId() == feed.getId());
+ } else {
+ assertTrue(expiredFeeds.isEmpty());
+ }
+ }
+
+ public void testGetExpiredFeedsListShouldReturnFeed() {
+ final long expirationTime = 1000 * 60 * 60; // 1 hour
+ expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime - 1, expirationTime, true);
+ }
+
+ public void testGetExpiredFeedsListShouldNotReturnFeed() {
+ final long expirationTime = 1000 * 60 * 60; // 1 hour
+ expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime / 2, expirationTime, false);
+ }
+
+
+ public void testGetFeedList() {
+ final Context context = getInstrumentation().getTargetContext();
+ List<Feed> feeds = saveFeedlist(context, 10, 0, false);
+ List<Feed> savedFeeds = DBReader.getFeedList(context);
+ assertNotNull(savedFeeds);
+ assertEquals(feeds.size(), savedFeeds.size());
+ for (int i = 0; i < feeds.size(); i++) {
+ assertTrue(savedFeeds.get(i).getId() == feeds.get(i).getId());
+ }
+ }
+
+ 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);
+ List<String> urls = DBReader.getFeedListDownloadUrls(context);
+ assertNotNull(urls);
+ assertTrue(urls.size() == feeds.size());
+ for (int i = 0; i < urls.size(); i++) {
+ assertEquals(urls.get(i), feeds.get(i).getDownload_url());
+ }
+ }
+
+ public void testLoadFeedDataOfFeedItemlist() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int numFeeds = 10;
+ final int numItems = 1;
+ List<Feed> feeds = saveFeedlist(context, numFeeds, numItems, false);
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ for (Feed f : feeds) {
+ for (FeedItem item : f.getItems()) {
+ item.setFeed(null);
+ item.setFeedId(f.getId());
+ items.add(item);
+ }
+ }
+ DBReader.loadFeedDataOfFeedItemlist(context, items);
+ for (int i = 0; i < numFeeds; i++) {
+ for (int j = 0; j < numItems; j++) {
+ FeedItem item = feeds.get(i).getItems().get(j);
+ assertNotNull(item.getFeed());
+ assertTrue(item.getFeed().getId() == feeds.get(i).getId());
+ assertTrue(item.getFeedId() == item.getFeed().getId());
+ }
+ }
+ }
+
+ public void testGetFeedItemList() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int numFeeds = 1;
+ final int numItems = 10;
+ Feed feed = saveFeedlist(context, numFeeds, numItems, false).get(0);
+ List<FeedItem> items = feed.getItems();
+ feed.setItems(null);
+ List<FeedItem> savedItems = DBReader.getFeedItemList(context, feed);
+ assertNotNull(savedItems);
+ assertTrue(savedItems.size() == items.size());
+ for (int i = 0; i < savedItems.size(); i++) {
+ assertTrue(items.get(i).getId() == savedItems.get(i).getId());
+ }
+ }
+
+ private List<FeedItem> saveQueue(int numItems) {
+ if (numItems <= 0) {
+ throw new IllegalArgumentException("numItems<=0");
+ }
+ final Context context = getInstrumentation().getTargetContext();
+ List<Feed> feeds = saveFeedlist(context, numItems, numItems, false);
+ List<FeedItem> allItems = new ArrayList<FeedItem>();
+ for (Feed f : feeds) {
+ allItems.addAll(f.getItems());
+ }
+ // take random items from every feed
+ Random random = new Random();
+ List<FeedItem> queue = new ArrayList<FeedItem>();
+ while (queue.size() < numItems) {
+ int index = random.nextInt(numItems);
+ if (!queue.contains(allItems.get(index))) {
+ queue.add(allItems.get(index));
+ }
+ }
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setQueue(queue);
+ adapter.close();
+ return queue;
+ }
+
+ public void testGetQueueIDList() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int numItems = 10;
+ List<FeedItem> queue = saveQueue(numItems);
+ List<Long> ids = DBReader.getQueueIDList(context);
+ assertNotNull(ids);
+ assertTrue(queue.size() == ids.size());
+ for (int i = 0; i < queue.size(); i++) {
+ assertTrue(ids.get(i) != 0);
+ assertTrue(queue.get(i).getId() == ids.get(i));
+ }
+ }
+
+ public void testGetQueue() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int numItems = 10;
+ List<FeedItem> queue = saveQueue(numItems);
+ List<FeedItem> savedQueue = DBReader.getQueue(context);
+ assertNotNull(savedQueue);
+ assertTrue(queue.size() == savedQueue.size());
+ for (int i = 0; i < queue.size(); i++) {
+ assertTrue(savedQueue.get(i).getId() != 0);
+ assertTrue(queue.get(i).getId() == savedQueue.get(i).getId());
+ }
+ }
+
+ private List<FeedItem> saveDownloadedItems(int numItems) {
+ if (numItems <= 0) {
+ throw new IllegalArgumentException("numItems<=0");
+ }
+ final Context context = getInstrumentation().getTargetContext();
+ List<Feed> feeds = saveFeedlist(context, numItems, numItems, true);
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ for (Feed f : feeds) {
+ items.addAll(f.getItems());
+ }
+ List<FeedItem> downloaded = new ArrayList<FeedItem>();
+ Random random = new Random();
+
+ while (downloaded.size() < numItems) {
+ int i = random.nextInt(numItems);
+ if (!downloaded.contains(items.get(i))) {
+ FeedItem item = items.get(i);
+ item.getMedia().setDownloaded(true);
+ item.getMedia().setFile_url("file" + i);
+ downloaded.add(item);
+ }
+ }
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedItemlist(downloaded);
+ adapter.close();
+ return downloaded;
+ }
+
+ public void testGetDownloadedItems() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int numItems = 10;
+ List<FeedItem> downloaded = saveDownloadedItems(numItems);
+ List<FeedItem> downloaded_saved = DBReader.getDownloadedItems(context);
+ assertNotNull(downloaded_saved);
+ assertTrue(downloaded_saved.size() == downloaded.size());
+ for (FeedItem item : downloaded_saved) {
+ assertNotNull(item.getMedia());
+ assertTrue(item.getMedia().isDownloaded());
+ assertNotNull(item.getMedia().getDownload_url());
+ }
+ }
+
+ private List<FeedItem> saveUnreadItems(int numItems) {
+ if (numItems <= 0) {
+ throw new IllegalArgumentException("numItems<=0");
+ }
+ final Context context = getInstrumentation().getTargetContext();
+ List<Feed> feeds = saveFeedlist(context, numItems, numItems, true);
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ for (Feed f : feeds) {
+ items.addAll(f.getItems());
+ }
+ List<FeedItem> unread = new ArrayList<FeedItem>();
+ Random random = new Random();
+
+ while (unread.size() < numItems) {
+ int i = random.nextInt(numItems);
+ if (!unread.contains(items.get(i))) {
+ FeedItem item = items.get(i);
+ item.setRead(false);
+ unread.add(item);
+ }
+ }
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedItemlist(unread);
+ adapter.close();
+ return unread;
+ }
+
+ public void testGetUnreadItemsList() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int numItems = 10;
+
+ List<FeedItem> unread = saveUnreadItems(numItems);
+ List<FeedItem> unreadSaved = DBReader.getUnreadItemsList(context);
+ assertNotNull(unreadSaved);
+ assertTrue(unread.size() == unreadSaved.size());
+ for (FeedItem item : unreadSaved) {
+ assertFalse(item.isRead());
+ }
+ }
+
+ public void testGetUnreadItemIds() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int numItems = 10;
+
+ List<FeedItem> unread = saveUnreadItems(numItems);
+ long[] unreadIds = new long[unread.size()];
+ for (int i = 0; i < unread.size(); i++) {
+ unreadIds[i] = unread.get(i).getId();
+ }
+ long[] unreadSaved = DBReader.getUnreadItemIds(context);
+ assertNotNull(unreadSaved);
+ assertTrue(unread.size() == unreadSaved.length);
+ for (long savedId : unreadSaved) {
+ boolean found = false;
+ for (long id : unreadIds) {
+ if (id == savedId) {
+ found = true;
+ break;
+ }
+ }
+ 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());
+ }
+ }
+
+ public void testGetNavDrawerDataQueueEmptyNoUnreadItems() {
+ 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, true);
+ DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData(context);
+ assertEquals(NUM_FEEDS, navDrawerData.feeds.size());
+ assertEquals(0, navDrawerData.numUnreadItems);
+ assertEquals(0, navDrawerData.queueSize);
+ }
+
+ public void testGetNavDrawerDataQueueNotEmptyWithUnreadItems() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int NUM_FEEDS = 10;
+ final int NUM_ITEMS = 10;
+ final int NUM_QUEUE = 1;
+ final int NUM_UNREAD = 2;
+ List<Feed> feeds = DBTestUtils.saveFeedlist(context, NUM_FEEDS, NUM_ITEMS, true);
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ for (int i = 0; i < NUM_UNREAD; i++) {
+ FeedItem item = feeds.get(0).getItems().get(i);
+ item.setRead(false);
+ adapter.setSingleFeedItem(item);
+ }
+ List<FeedItem> queue = new ArrayList<FeedItem>();
+ for (int i = 0; i < NUM_QUEUE; i++) {
+ FeedItem item = feeds.get(1).getItems().get(i);
+ queue.add(item);
+ }
+ adapter.setQueue(queue);
+
+ adapter.close();
+
+ DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData(context);
+ assertEquals(NUM_FEEDS, navDrawerData.feeds.size());
+ assertEquals(NUM_UNREAD, navDrawerData.numUnreadItems);
+ assertEquals(NUM_QUEUE, navDrawerData.queueSize);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java
new file mode 100644
index 000000000..fd5b1c393
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java
@@ -0,0 +1,326 @@
+package de.test.antennapod.storage;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import static de.test.antennapod.storage.DBTestUtils.*;
+
+/**
+ * Test class for DBTasks
+ */
+public class DBTasksTest extends InstrumentationTestCase {
+ private static final String TEST_FOLDER = "testDBTasks";
+ private static final int EPISODE_CACHE_SIZE = 5;
+
+ private File destFolder;
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ final Context context = getInstrumentation().getTargetContext();
+ assertTrue(PodDBAdapter.deleteDatabase(context));
+
+ for (File f : destFolder.listFiles()) {
+ assertTrue(f.delete());
+ }
+ assertTrue(destFolder.delete());
+
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+ assertTrue(destFolder.exists());
+ assertTrue(destFolder.canWrite());
+
+ final Context context = getInstrumentation().getTargetContext();
+ context.deleteDatabase(PodDBAdapter.DATABASE_NAME);
+ // make sure database is created
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.close();
+
+ SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext().getApplicationContext()).edit();
+ prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE));
+ prefEdit.commit();
+ }
+
+ public void testPerformAutoCleanupShouldDelete() throws IOException {
+ final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", new Date(), "title");
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<File>();
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed);
+
+ 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), 0));
+ items.add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : items) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+ DBTasks.performAutoCleanup(getInstrumentation().getTargetContext());
+ for (int i = 0; i < files.size(); i++) {
+ if (i < EPISODE_CACHE_SIZE) {
+ assertTrue(files.get(i).exists());
+ } else {
+ assertFalse(files.get(i).exists());
+ }
+ }
+ }
+
+ public void testPerformAutoCleanupShouldNotDeleteBecauseUnread() throws IOException {
+ final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", new Date(), "title");
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<File>();
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), false, feed);
+
+ File f = new File(destFolder, "file " + i);
+ 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), 0));
+ items.add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : items) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+ DBTasks.performAutoCleanup(getInstrumentation().getTargetContext());
+ for (File file : files) {
+ assertTrue(file.exists());
+ }
+ }
+
+ public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException {
+ final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", new Date(), "title");
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<File>();
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), true, feed);
+
+ File f = new File(destFolder, "file " + i);
+ 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), 0));
+ items.add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.setQueue(items);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : items) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+ DBTasks.performAutoCleanup(getInstrumentation().getTargetContext());
+ for (File file : files) {
+ assertTrue(file.exists());
+ }
+ }
+
+ /**
+ * Reproduces a bug where DBTasks.performAutoCleanup(android.content.Context) would use the ID of the FeedItem in the
+ * call to DBWriter.deleteFeedMediaOfItem instead of the ID of the FeedMedia. This would cause the wrong item to be deleted.
+ * @throws IOException
+ */
+ public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException {
+ final Context context = getInstrumentation().getTargetContext();
+ // add feed with no enclosures so that item ID != media ID
+ saveFeedlist(context, 1, 10, false);
+
+ // add candidate for performAutoCleanup
+ List<Feed> feeds = saveFeedlist(getInstrumentation().getTargetContext(), 1, 1, true);
+ FeedMedia m = feeds.get(0).getItems().get(0).getMedia();
+ m.setDownloaded(true);
+ m.setFile_url("file");
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setMedia(m);
+ adapter.close();
+
+ testPerformAutoCleanupShouldNotDeleteBecauseInQueue();
+ }
+
+ public void testUpdateFeedNewFeed() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int NUM_ITEMS = 10;
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(), false, feed));
+ }
+ Feed newFeed = DBTasks.updateFeed(context, feed)[0];
+
+ assertTrue(newFeed == feed);
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertFalse(item.isRead());
+ assertTrue(item.getId() != 0);
+ }
+ }
+
+ /** Two feeds with the same title, but different download URLs should be treated as different feeds. */
+ public void testUpdateFeedSameTitle() {
+ final Context context = getInstrumentation().getTargetContext();
+
+ Feed feed1 = new Feed("url1", new Date(), "title");
+ Feed feed2 = new Feed("url2", new Date(), "title");
+
+ feed1.setItems(new ArrayList<FeedItem>());
+ feed2.setItems(new ArrayList<FeedItem>());
+
+ Feed savedFeed1 = DBTasks.updateFeed(context, feed1)[0];
+ Feed savedFeed2 = DBTasks.updateFeed(context, feed2)[0];
+
+ assertTrue(savedFeed1.getId() != savedFeed2.getId());
+ }
+
+ public void testUpdateFeedUpdatedFeed() {
+ final Context context = getInstrumentation().getTargetContext();
+ final int NUM_ITEMS_OLD = 10;
+ final int NUM_ITEMS_NEW = 10;
+
+ final Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+ for (int i = 0; i < NUM_ITEMS_OLD; i++) {
+ feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), true, feed));
+ }
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ // ensure that objects have been saved in db, then reset
+ assertTrue(feed.getId() != 0);
+ final long feedID = feed.getId();
+ feed.setId(0);
+ List<Long> itemIDs = new ArrayList<Long>();
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ itemIDs.add(item.getId());
+ item.setId(0);
+ }
+
+ for (int i = NUM_ITEMS_OLD; i < NUM_ITEMS_NEW + NUM_ITEMS_OLD; i++) {
+ feed.getItems().add(0, new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), true, feed));
+ }
+
+ final Feed newFeed = DBTasks.updateFeed(context, feed)[0];
+ assertTrue(feed != newFeed);
+
+ updatedFeedTest(newFeed, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW);
+
+ final Feed feedFromDB = DBReader.getFeed(context, newFeed.getId());
+ assertNotNull(feedFromDB);
+ assertTrue(feedFromDB.getId() == newFeed.getId());
+ updatedFeedTest(feedFromDB, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW);
+ }
+
+ private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs, final int NUM_ITEMS_OLD, final int NUM_ITEMS_NEW) {
+ assertTrue(newFeed.getId() == feedID);
+ assertTrue(newFeed.getItems().size() == NUM_ITEMS_NEW + NUM_ITEMS_OLD);
+ Collections.reverse(newFeed.getItems());
+ Date lastDate = new Date(0);
+ for (int i = 0; i < NUM_ITEMS_OLD; i++) {
+ FeedItem item = newFeed.getItems().get(i);
+ assertTrue(item.getFeed() == newFeed);
+ assertTrue(item.getId() == itemIDs.get(i));
+ assertTrue(item.isRead());
+ assertTrue(item.getPubDate().getTime() >= lastDate.getTime());
+ lastDate = item.getPubDate();
+ }
+ for (int i = NUM_ITEMS_OLD; i < NUM_ITEMS_NEW + NUM_ITEMS_OLD; i++) {
+ FeedItem item = newFeed.getItems().get(i);
+ assertTrue(item.getFeed() == newFeed);
+ assertTrue(item.getId() != 0);
+ assertFalse(item.isRead());
+ assertTrue(item.getPubDate().getTime() >= lastDate.getTime());
+ lastDate = item.getPubDate();
+ }
+ }
+
+ private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) {
+ 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, new FlattrStatus());
+ feed.setItems(new ArrayList<FeedItem>());
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ List<Feed> expiredFeeds = DBTasks.getExpiredFeeds(context);
+ assertNotNull(expiredFeeds);
+ if (shouldReturn) {
+ assertTrue(expiredFeeds.size() == 1);
+ assertTrue(expiredFeeds.get(0).getId() == feed.getId());
+ } else {
+ assertTrue(expiredFeeds.isEmpty());
+ }
+ }
+
+ public void testGetExpiredFeedsTestShouldReturn() {
+ final long expirationTime = 1000 * 60 * 60;
+ expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime - 1, expirationTime, true);
+ }
+
+ public void testGetExpiredFeedsTestShouldNotReturn() {
+ final long expirationTime = 1000 * 60 * 60;
+ expiredFeedListTestHelper(System.currentTimeMillis() - expirationTime / 2, expirationTime, false);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java
new file mode 100644
index 000000000..e7d6396f5
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java
@@ -0,0 +1,57 @@
+package de.test.antennapod.storage;
+
+import android.content.Context;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Utility methods for DB* tests.
+ */
+public class DBTestUtils {
+
+ public static List<Feed> saveFeedlist(Context context, int numFeeds, int numItems, boolean withMedia) {
+ if (numFeeds <= 0) {
+ throw new IllegalArgumentException("numFeeds<=0");
+ }
+ if (numItems < 0) {
+ throw new IllegalArgumentException("numItems<0");
+ }
+
+ List<Feed> feeds = new ArrayList<Feed>();
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ 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, 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(),
+ true, f);
+ if (withMedia) {
+ FeedMedia media = new FeedMedia(item, "url" + j, 1, "audio/mp3");
+ item.setMedia(media);
+ }
+ f.getItems().add(item);
+ }
+ Collections.sort(f.getItems(), new FeedItemPubdateComparator());
+ adapter.setCompleteFeed(f);
+ Assert.assertTrue(f.getId() != 0);
+ for (FeedItem item : f.getItems()) {
+ Assert.assertTrue(item.getId() != 0);
+ }
+ feeds.add(f);
+ }
+ adapter.close();
+ return feeds;
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
new file mode 100644
index 000000000..4678a843b
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
@@ -0,0 +1,796 @@
+package de.test.antennapod.storage;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedImage;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Test class for DBWriter
+ */
+public class DBWriterTest extends InstrumentationTestCase {
+ private static final String TAG = "DBWriterTest";
+ private static final String TEST_FOLDER = "testDBWriter";
+ private static final long TIMEOUT = 5L;
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ final Context context = getInstrumentation().getTargetContext();
+ assertTrue(PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()));
+
+ File testDir = context.getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(testDir);
+ for (File f : testDir.listFiles()) {
+ f.delete();
+ }
+ }
+
+ @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 testDeleteFeedMediaOfItemFileExists() throws IOException, ExecutionException, InterruptedException {
+ File dest = new File(getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile");
+
+ assertTrue(dest.createNewFile());
+
+ Feed feed = new Feed("url", new Date(), "title");
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ 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, 0);
+ item.setMedia(media);
+
+ items.add(item);
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+ assertTrue(media.getId() != 0);
+ assertTrue(item.getId() != 0);
+
+ DBWriter.deleteFeedMediaOfItem(getInstrumentation().getTargetContext(), media.getId()).get();
+ media = DBReader.getFeedMedia(getInstrumentation().getTargetContext(), media.getId());
+ assertNotNull(media);
+ assertFalse(dest.exists());
+ assertFalse(media.isDownloaded());
+ assertNull(media.getFile_url());
+ }
+
+ public void testDeleteFeed() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+
+ // create Feed image
+ File imgFile = new File(destFolder, "image");
+ assertTrue(imgFile.createNewFile());
+ FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true);
+ image.setOwner(feed);
+ feed.setImage(image);
+
+ List<File> itemFiles = new ArrayList<File>();
+ // create items with downloaded media files
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed);
+ feed.getItems().add(item);
+
+ File enc = new File(destFolder, "file " + i);
+ assertTrue(enc.createNewFile());
+ itemFiles.add(enc);
+
+ FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0);
+ item.setMedia(media);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ assertTrue(feed.getImage().getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+
+ DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ // check if files still exist
+ assertFalse(imgFile.exists());
+ for (File f : itemFiles) {
+ assertFalse(f.exists());
+ }
+
+ adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getImageCursor(image.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getSingleFeedMediaCursor(item.getMedia().getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ }
+ }
+
+ public void testDeleteFeedNoImage() throws ExecutionException, InterruptedException, IOException, TimeoutException {
+ File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+
+ feed.setImage(null);
+
+ List<File> itemFiles = new ArrayList<File>();
+ // create items with downloaded media files
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed);
+ feed.getItems().add(item);
+
+ File enc = new File(destFolder, "file " + i);
+ assertTrue(enc.createNewFile());
+
+ itemFiles.add(enc);
+ FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0);
+ item.setMedia(media);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+
+ DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ // check if files still exist
+ for (File f : itemFiles) {
+ assertFalse(f.exists());
+ }
+
+ adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getSingleFeedMediaCursor(item.getMedia().getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ }
+ }
+
+ public void testDeleteFeedNoItems() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(null);
+
+ // create Feed image
+ File imgFile = new File(destFolder, "image");
+ assertTrue(imgFile.createNewFile());
+ FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true);
+ image.setOwner(feed);
+ feed.setImage(image);
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ assertTrue(feed.getImage().getId() != 0);
+
+ DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ // check if files still exist
+ assertFalse(imgFile.exists());
+
+ adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getImageCursor(image.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ }
+
+ public void testDeleteFeedNoFeedMedia() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+
+ // create Feed image
+ File imgFile = new File(destFolder, "image");
+ assertTrue(imgFile.createNewFile());
+ FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true);
+ image.setOwner(feed);
+ feed.setImage(image);
+
+ // create items
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed);
+ feed.getItems().add(item);
+
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ assertTrue(feed.getImage().getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+
+ DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ // check if files still exist
+ assertFalse(imgFile.exists());
+
+ adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getImageCursor(image.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertTrue(c.getCount() == 0);
+ c.close();
+ }
+ }
+
+ public void testDeleteFeedWithItemImages() throws InterruptedException, ExecutionException, TimeoutException, IOException {
+ File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+
+ // create Feed image
+ File imgFile = new File(destFolder, "image");
+ assertTrue(imgFile.createNewFile());
+ FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true);
+ image.setOwner(feed);
+ feed.setImage(image);
+
+ // create items with images
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed);
+ feed.getItems().add(item);
+ File itemImageFile = new File(destFolder, "item-image-" + i);
+ FeedImage itemImage = new FeedImage(0, "item-image" + i, itemImageFile.getAbsolutePath(), "url", true);
+ item.setImage(itemImage);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ assertTrue(feed.getImage().getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getImage().getId() != 0);
+ }
+
+ DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ // check if files still exist
+ assertFalse(imgFile.exists());
+
+ adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getImageCursor(image.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getImageCursor(item.getImage().getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+ }
+
+ public void testDeleteFeedWithQueueItems() throws ExecutionException, InterruptedException, TimeoutException {
+ File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+
+ // create Feed image
+ File imgFile = new File(destFolder, "image");
+ FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true);
+ image.setOwner(feed);
+ feed.setImage(image);
+
+ List<File> itemFiles = new ArrayList<File>();
+ // create items with downloaded media files
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed);
+ feed.getItems().add(item);
+
+ 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, 0);
+ item.setMedia(media);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ assertTrue(feed.getImage().getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+
+
+ List<FeedItem> queue = new ArrayList<FeedItem>();
+ queue.addAll(feed.getItems());
+ adapter.open();
+ adapter.setQueue(queue);
+
+ Cursor queueCursor = adapter.getQueueIDCursor();
+ assertTrue(queueCursor.getCount() == queue.size());
+ queueCursor.close();
+
+ adapter.close();
+ DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter.open();
+
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getImageCursor(image.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getSingleFeedMediaCursor(item.getMedia().getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ }
+ c = adapter.getQueueCursor();
+ assertTrue(c.getCount() == 0);
+ c.close();
+ adapter.close();
+ }
+
+ public void testDeleteFeedNoDownloadedFiles() throws ExecutionException, InterruptedException, TimeoutException {
+ File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+
+ // create Feed image
+ File imgFile = new File(destFolder, "image");
+ FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true);
+ image.setOwner(feed);
+ feed.setImage(image);
+
+ List<File> itemFiles = new ArrayList<File>();
+ // create items with downloaded media files
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), true, feed);
+ feed.getItems().add(item);
+
+ 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, 0);
+ item.setMedia(media);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ assertTrue(feed.getImage().getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+
+ DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = new PodDBAdapter(getInstrumentation().getContext());
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getImageCursor(image.getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertTrue(c.getCount() == 0);
+ c.close();
+ c = adapter.getSingleFeedMediaCursor(item.getMedia().getId());
+ assertTrue(c.getCount() == 0);
+ c.close();
+ }
+ }
+
+ private FeedMedia playbackHistorySetup(Date playbackCompletionDate) {
+ final Context context = getInstrumentation().getTargetContext();
+ 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, 0);
+ feed.getItems().add(item);
+ item.setMedia(media);
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+ assertTrue(media.getId() != 0);
+ return media;
+ }
+
+ public void testAddItemToPlaybackHistoryNotPlayedYet() throws ExecutionException, InterruptedException {
+ final Context context = getInstrumentation().getTargetContext();
+
+ FeedMedia media = playbackHistorySetup(null);
+ DBWriter.addItemToPlaybackHistory(context, media).get();
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ media = DBReader.getFeedMedia(context, media.getId());
+ adapter.close();
+
+ assertNotNull(media);
+ assertNotNull(media.getPlaybackCompletionDate());
+ }
+
+ public void testAddItemToPlaybackHistoryAlreadyPlayed() throws ExecutionException, InterruptedException {
+ final long OLD_DATE = 0;
+ final Context context = getInstrumentation().getTargetContext();
+
+ FeedMedia media = playbackHistorySetup(new Date(OLD_DATE));
+ DBWriter.addItemToPlaybackHistory(getInstrumentation().getTargetContext(), media).get();
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ media = DBReader.getFeedMedia(context, media.getId());
+ adapter.close();
+
+ assertNotNull(media);
+ assertNotNull(media.getPlaybackCompletionDate());
+ assertFalse(OLD_DATE == media.getPlaybackCompletionDate().getTime());
+ }
+
+ private Feed queueTestSetupMultipleItems(final int NUM_ITEMS) throws InterruptedException, ExecutionException, TimeoutException {
+ final Context context = getInstrumentation().getTargetContext();
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), true, feed);
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ List<Future<?>> futures = new ArrayList<Future<?>>();
+ for (FeedItem item : feed.getItems()) {
+ futures.add(DBWriter.addQueueItem(context, item.getId()));
+ }
+ for (Future<?> f : futures) {
+ f.get(TIMEOUT, TimeUnit.SECONDS);
+ }
+ return feed;
+ }
+
+ public void testAddQueueItemSingleItem() throws InterruptedException, ExecutionException, TimeoutException {
+ final Context context = getInstrumentation().getTargetContext();
+ 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);
+ feed.getItems().add(item);
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(item.getId() != 0);
+ DBWriter.addQueueItem(context, item.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertTrue(cursor.getLong(0) == item.getId());
+ cursor.close();
+ adapter.close();
+ }
+
+ public void testAddQueueItemSingleItemAlreadyInQueue() throws InterruptedException, ExecutionException, TimeoutException {
+ final Context context = getInstrumentation().getTargetContext();
+ 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);
+ feed.getItems().add(item);
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(item.getId() != 0);
+ DBWriter.addQueueItem(context, item.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertTrue(cursor.getLong(0) == item.getId());
+ cursor.close();
+ adapter.close();
+
+ DBWriter.addQueueItem(context, item.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter = new PodDBAdapter(context);
+ adapter.open();
+ cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertTrue(cursor.getLong(0) == item.getId());
+ assertTrue(cursor.getCount() == 1);
+ cursor.close();
+ adapter.close();
+ }
+
+ public void testAddQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException {
+ final Context context = getInstrumentation().getTargetContext();
+ final int NUM_ITEMS = 10;
+
+ Feed feed = queueTestSetupMultipleItems(NUM_ITEMS);
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertTrue(cursor.getCount() == NUM_ITEMS);
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ assertTrue(cursor.moveToPosition(i));
+ assertTrue(cursor.getLong(0) == feed.getItems().get(i).getId());
+ }
+ cursor.close();
+ adapter.close();
+ }
+
+ public void testClearQueue() throws InterruptedException, ExecutionException, TimeoutException {
+ final Context context = getInstrumentation().getTargetContext();
+ final int NUM_ITEMS = 10;
+
+ Feed feed = queueTestSetupMultipleItems(NUM_ITEMS);
+ DBWriter.clearQueue(context).get(TIMEOUT, TimeUnit.SECONDS);
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertFalse(cursor.moveToFirst());
+ cursor.close();
+ adapter.close();
+ }
+
+ public void testRemoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException {
+ final int NUM_ITEMS = 10;
+ final Context context = getInstrumentation().getTargetContext();
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), true, feed);
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ for (int removeIndex = 0; removeIndex < NUM_ITEMS; removeIndex++) {
+ final long id = feed.getItems().get(removeIndex).getId();
+ adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setQueue(feed.getItems());
+ adapter.close();
+
+ DBWriter.removeQueueItem(context, id, false).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor queue = adapter.getQueueIDCursor();
+ assertTrue(queue.getCount() == NUM_ITEMS - 1);
+ for (int i = 0; i < queue.getCount(); i++) {
+ assertTrue(queue.moveToPosition(i));
+ final long queueID = queue.getLong(0);
+ assertTrue(queueID != id); // removed item is no longer in queue
+ boolean idFound = false;
+ for (FeedItem item : feed.getItems()) { // items that were not removed are still in the queue
+ idFound = idFound | (item.getId() == queueID);
+ }
+ assertTrue(idFound);
+ }
+
+ queue.close();
+ adapter.close();
+ }
+ }
+
+ public void testMoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException {
+ final int NUM_ITEMS = 10;
+ final Context context = getInstrumentation().getTargetContext();
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), true, feed);
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ for (int from = 0; from < NUM_ITEMS; from++) {
+ for (int to = 0; to < NUM_ITEMS; to++) {
+ if (from == to) {
+ continue;
+ }
+ Log.d(TAG, String.format("testMoveQueueItem: From=%d, To=%d", from, to));
+ final long fromID = feed.getItems().get(from).getId();
+
+ adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setQueue(feed.getItems());
+ adapter.close();
+
+ DBWriter.moveQueueItem(context, from, to, false).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor queue = adapter.getQueueIDCursor();
+ assertTrue(queue.getCount() == NUM_ITEMS);
+ assertTrue(queue.moveToPosition(from));
+ assertFalse(queue.getLong(0) == fromID);
+ assertTrue(queue.moveToPosition(to));
+ assertTrue(queue.getLong(0) == fromID);
+
+ queue.close();
+ adapter.close();
+ }
+ }
+ }
+
+ public void testMarkFeedRead() throws InterruptedException, ExecutionException, TimeoutException {
+ final Context context = getInstrumentation().getTargetContext();
+ final int NUM_ITEMS = 10;
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), false, feed);
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+
+ DBWriter.markFeedRead(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+ List<FeedItem> loadedItems = DBReader.getFeedItemList(context, feed);
+ for (FeedItem item : loadedItems) {
+ assertTrue(item.isRead());
+ }
+ }
+
+ public void testMarkAllItemsReadSameFeed() throws InterruptedException, ExecutionException, TimeoutException {
+ final Context context = getInstrumentation().getTargetContext();
+ final int NUM_ITEMS = 10;
+ Feed feed = new Feed("url", new Date(), "title");
+ feed.setItems(new ArrayList<FeedItem>());
+ for (int i = 0; i < NUM_ITEMS; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), false, feed);
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+
+ DBWriter.markAllItemsRead(context).get(TIMEOUT, TimeUnit.SECONDS);
+ List<FeedItem> loadedItems = DBReader.getFeedItemList(context, feed);
+ for (FeedItem item : loadedItems) {
+ assertTrue(item.isRead());
+ }
+ }
+
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java
new file mode 100644
index 000000000..3656582e1
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java
@@ -0,0 +1,115 @@
+package de.test.antennapod.ui;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+import com.robotium.solo.Solo;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.activity.PreferenceActivity;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+
+/**
+ * User interface tests for MainActivity
+ */
+public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
+
+ private Solo solo;
+ private UITestUtils uiTestUtils;
+
+ public MainActivityTest() {
+ super(MainActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ solo = new Solo(getInstrumentation(), getActivity());
+ uiTestUtils = new UITestUtils(getInstrumentation().getTargetContext());
+ uiTestUtils.setup();
+ // create database
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext());
+ adapter.open();
+ adapter.close();
+
+ // override first launch preference
+ SharedPreferences prefs = getInstrumentation().getTargetContext().getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false).commit();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ uiTestUtils.tearDown();
+ solo.finishOpenedActivities();
+ PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext());
+ super.tearDown();
+ }
+
+ public void testAddFeed() throws Exception {
+ uiTestUtils.addHostedFeedData();
+ final Feed feed = uiTestUtils.hostedFeeds.get(0);
+ solo.setNavigationDrawer(Solo.OPENED);
+ solo.clickOnText(solo.getString(R.string.add_feed_label));
+ solo.enterText(0, feed.getDownload_url());
+ solo.clickOnButton(0);
+ solo.waitForActivity(DefaultOnlineFeedViewActivity.class);
+ solo.waitForView(R.id.butSubscribe);
+ assertEquals(solo.getString(R.string.subscribe_label), solo.getButton(0).getText().toString());
+ solo.clickOnButton(0);
+ solo.waitForText(solo.getString(R.string.subscribed_label));
+ }
+
+ public void testClickNavDrawer() throws Exception {
+ uiTestUtils.addLocalFeedData(false);
+ final View home = solo.getView(UITestUtils.HOME_VIEW);
+
+ // all episodes
+ solo.waitForView(android.R.id.list);
+ assertEquals(solo.getString(R.string.all_episodes_label), getActionbarTitle());
+ // queue
+ solo.clickOnView(home);
+ solo.clickOnText(solo.getString(R.string.queue_label));
+ solo.waitForView(android.R.id.list);
+ assertEquals(solo.getString(R.string.queue_label), getActionbarTitle());
+
+ // downloads
+ solo.clickOnView(home);
+ solo.clickOnText(solo.getString(R.string.downloads_label));
+ solo.waitForView(android.R.id.list);
+ assertEquals(solo.getString(R.string.downloads_label), getActionbarTitle());
+
+ // playback history
+ solo.clickOnView(home);
+ solo.clickOnText(solo.getString(R.string.playback_history_label));
+ solo.waitForView(android.R.id.list);
+ assertEquals(solo.getString(R.string.playback_history_label), getActionbarTitle());
+
+ // add podcast
+ solo.clickOnView(home);
+ solo.clickOnText(solo.getString(R.string.add_feed_label));
+ solo.waitForView(R.id.txtvFeedurl);
+ assertEquals(solo.getString(R.string.add_feed_label), getActionbarTitle());
+
+ // podcasts
+ for (int i = 0; i < uiTestUtils.hostedFeeds.size(); i++) {
+ Feed f = uiTestUtils.hostedFeeds.get(i);
+ solo.clickOnView(home);
+ solo.clickOnText(f.getTitle());
+ solo.waitForView(android.R.id.list);
+ assertEquals("", getActionbarTitle());
+ }
+ }
+
+ private String getActionbarTitle() {
+ return ((MainActivity)solo.getCurrentActivity()).getMainActivtyActionBar().getTitle().toString();
+ }
+
+ public void testGoToPreferences() {
+ solo.setNavigationDrawer(Solo.CLOSED);
+ solo.clickOnMenuItem(solo.getString(R.string.settings_label));
+ solo.waitForActivity(PreferenceActivity.class);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java
new file mode 100644
index 000000000..daae4bd62
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java
@@ -0,0 +1,149 @@
+package de.test.antennapod.ui;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.TextView;
+import com.robotium.solo.Solo;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.AudioplayerActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+
+import java.util.List;
+
+/**
+ * Test cases for starting and ending playback from the MainActivity and AudioPlayerActivity
+ */
+public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> {
+
+ private Solo solo;
+ private UITestUtils uiTestUtils;
+
+ public PlaybackTest() {
+ super(MainActivity.class);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ solo = new Solo(getInstrumentation(), getActivity());
+ uiTestUtils = new UITestUtils(getInstrumentation().getTargetContext());
+ uiTestUtils.setup();
+ // create database
+ PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext());
+ adapter.open();
+ adapter.close();
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext());
+ prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT, false).commit();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ uiTestUtils.tearDown();
+ solo.finishOpenedActivities();
+ PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext());
+
+ // shut down playback service
+ skipEpisode();
+ getInstrumentation().getTargetContext().sendBroadcast(
+ new Intent(PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+
+ super.tearDown();
+ }
+
+ private void setContinuousPlaybackPreference(boolean value) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext());
+ prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).commit();
+ }
+
+ private void skipEpisode() {
+ Intent skipIntent = new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE);
+ getInstrumentation().getTargetContext().sendBroadcast(skipIntent);
+ }
+
+ private void startLocalPlayback() {
+ assertTrue(solo.waitForActivity(MainActivity.class));
+ solo.setNavigationDrawer(Solo.CLOSED);
+ solo.clickOnView(solo.getView(R.id.butSecondaryAction));
+ assertTrue(solo.waitForActivity(AudioplayerActivity.class));
+ assertTrue(solo.waitForView(solo.getView(R.id.butPlay)));
+ }
+
+ private void startLocalPlaybackFromQueue() {
+ assertTrue(solo.waitForActivity(MainActivity.class));
+ solo.clickOnView(solo.getView(UITestUtils.HOME_VIEW));
+ solo.clickOnText(solo.getString(R.string.queue_label));
+ assertTrue(solo.waitForView(solo.getView(R.id.butSecondaryAction)));
+ solo.clickOnImageButton(0);
+ assertTrue(solo.waitForActivity(AudioplayerActivity.class));
+ assertTrue(solo.waitForView(solo.getView(R.id.butPlay)));
+ }
+
+ public void testStartLocal() throws Exception {
+ uiTestUtils.addLocalFeedData(true);
+ DBWriter.clearQueue(getInstrumentation().getTargetContext()).get();
+ startLocalPlayback();
+
+ solo.clickOnView(solo.getView(R.id.butPlay));
+ }
+
+ public void testContinousPlaybackOffSingleEpisode() throws Exception {
+ setContinuousPlaybackPreference(false);
+ uiTestUtils.addLocalFeedData(true);
+ DBWriter.clearQueue(getInstrumentation().getTargetContext()).get();
+ startLocalPlayback();
+ assertTrue(solo.waitForActivity(MainActivity.class));
+ }
+
+
+ public void testContinousPlaybackOffMultipleEpisodes() throws Exception {
+ setContinuousPlaybackPreference(false);
+ uiTestUtils.addLocalFeedData(true);
+ List<FeedItem> queue = DBReader.getQueue(getInstrumentation().getTargetContext());
+ FeedItem second = queue.get(1);
+
+ startLocalPlaybackFromQueue();
+ assertTrue(solo.waitForText(second.getTitle()));
+ }
+
+ public void testContinuousPlaybackOnMultipleEpisodes() throws Exception {
+ setContinuousPlaybackPreference(true);
+ uiTestUtils.addLocalFeedData(true);
+ List<FeedItem> queue = DBReader.getQueue(getInstrumentation().getTargetContext());
+ FeedItem second = queue.get(1);
+
+ startLocalPlaybackFromQueue();
+ assertTrue(solo.waitForText(second.getTitle()));
+ }
+
+ /**
+ * Check if an episode can be played twice without problems.
+ */
+ private void replayEpisodeCheck(boolean followQueue) throws Exception {
+ setContinuousPlaybackPreference(followQueue);
+ uiTestUtils.addLocalFeedData(true);
+ DBWriter.clearQueue(getInstrumentation().getTargetContext()).get();
+ String title = ((TextView) solo.getView(R.id.txtvTitle)).getText().toString();
+ startLocalPlayback();
+ assertTrue(solo.waitForText(title));
+ assertTrue(solo.waitForActivity(MainActivity.class));
+ startLocalPlayback();
+ assertTrue(solo.waitForText(title));
+ assertTrue(solo.waitForActivity(MainActivity.class));
+ }
+
+ public void testReplayEpisodeContinuousPlaybackOn() throws Exception {
+ replayEpisodeCheck(true);
+ }
+
+ public void testReplayEpisodeContinuousPlaybackOff() throws Exception {
+ replayEpisodeCheck(false);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java
new file mode 100644
index 000000000..55fffb80a
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java
@@ -0,0 +1,200 @@
+package de.test.antennapod.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Build;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.*;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.test.antennapod.util.service.download.HTTPBin;
+import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator;
+import junit.framework.Assert;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Utility methods for UI tests.
+ * Starts a web server that hosts feeds, episodes and images.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class UITestUtils {
+
+ private static final String DATA_FOLDER = "test/UITestUtils";
+
+ public static final int NUM_FEEDS = 5;
+ public static final int NUM_ITEMS_PER_FEED = 10;
+
+ public static final int HOME_VIEW = (Build.VERSION.SDK_INT >= 11) ? android.R.id.home : R.id.home;
+
+
+ private Context context;
+ private HTTPBin server = new HTTPBin();
+ private File destDir;
+ private File hostedFeedDir;
+ private File hostedMediaDir;
+
+ public List<Feed> hostedFeeds = new ArrayList<Feed>();
+
+ public UITestUtils(Context context) {
+ this.context = context;
+ }
+
+
+ public void setup() throws IOException {
+ destDir = context.getExternalFilesDir(DATA_FOLDER);
+ destDir.mkdir();
+ hostedFeedDir = new File(destDir, "hostedFeeds");
+ hostedFeedDir.mkdir();
+ hostedMediaDir = new File(destDir, "hostedMediaDir");
+ hostedMediaDir.mkdir();
+ Assert.assertTrue(destDir.exists());
+ Assert.assertTrue(hostedFeedDir.exists());
+ Assert.assertTrue(hostedMediaDir.exists());
+ server.start();
+ }
+
+ public void tearDown() throws IOException {
+ FileUtils.deleteDirectory(destDir);
+ FileUtils.deleteDirectory(hostedMediaDir);
+ FileUtils.deleteDirectory(hostedFeedDir);
+ server.stop();
+
+ if (localFeedDataAdded) {
+ PodDBAdapter.deleteDatabase(context);
+ }
+ }
+
+ private String hostFeed(Feed feed) throws IOException {
+ File feedFile = new File(hostedFeedDir, feed.getTitle());
+ FileOutputStream out = new FileOutputStream(feedFile);
+ RSS2Generator generator = new RSS2Generator();
+ generator.writeFeed(feed, out, "UTF-8", 0);
+ out.close();
+ int id = server.serveFile(feedFile);
+ Assert.assertTrue(id != -1);
+ return String.format("%s/files/%d", HTTPBin.BASE_URL, id);
+ }
+
+ private String hostFile(File file) {
+ int id = server.serveFile(file);
+ Assert.assertTrue(id != -1);
+ return String.format("%s/files/%d", HTTPBin.BASE_URL, id);
+ }
+
+ private File newBitmapFile(String name) throws IOException {
+ File imgFile = new File(destDir, name);
+ Bitmap bitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888);
+ FileOutputStream out = new FileOutputStream(imgFile);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 1, out);
+ out.close();
+ return imgFile;
+ }
+
+ private File newMediaFile(String name) throws IOException {
+ File mediaFile = new File(hostedMediaDir, name);
+ Assert.assertFalse(mediaFile.exists());
+
+ InputStream in = context.getAssets().open("testfile.mp3");
+ Assert.assertNotNull(in);
+
+ FileOutputStream out = new FileOutputStream(mediaFile);
+ IOUtils.copy(in, out);
+ out.close();
+
+ return mediaFile;
+ }
+
+ private boolean feedDataHosted = false;
+
+ /**
+ * Adds feeds, images and episodes to the webserver for testing purposes.
+ */
+ public void addHostedFeedData() throws IOException {
+ if (feedDataHosted) throw new IllegalStateException("addHostedFeedData was called twice on the same instance");
+ for (int i = 0; i < NUM_FEEDS; i++) {
+ File bitmapFile = newBitmapFile("image" + i);
+ FeedImage image = new FeedImage(0, "image " + i, null, hostFile(bitmapFile), false);
+ Feed feed = new Feed(0, new Date(), "Title " + i, "http://example.com/" + i, "Description of feed " + i,
+ "http://example.com/pay/feed" + i, "author " + i, "en", Feed.TYPE_RSS2, "feed" + i, image, null,
+ "http://example.com/feed/src/" + i, false);
+ image.setOwner(feed);
+
+ // create items
+ List<FeedItem> items = new ArrayList<FeedItem>();
+ for (int j = 0; j < NUM_ITEMS_PER_FEED; j++) {
+ FeedItem item = new FeedItem(0, "item" + j, "item" + j, "http://example.com/feed" + i + "/item/" + j, new Date(), true, feed);
+ items.add(item);
+
+ File mediaFile = newMediaFile("feed-" + i + "-episode-" + j + ".mp3");
+ item.setMedia(new FeedMedia(0, item, 0, 0, mediaFile.length(), "audio/mp3", null, hostFile(mediaFile), false, null, 0));
+
+ }
+ feed.setItems(items);
+ feed.setDownload_url(hostFeed(feed));
+ hostedFeeds.add(feed);
+ }
+ feedDataHosted = true;
+ }
+
+
+ private boolean localFeedDataAdded = false;
+
+ /**
+ * Adds feeds, images and episodes to the local database. This method will also call addHostedFeedData if it has not
+ * been called yet.
+ *
+ * Adds one item of each feed to the queue and to the playback history.
+ *
+ * This method should NOT be called if the testing class wants to download the hosted feed data.
+ *
+ * @param downloadEpisodes true if episodes should also be marked as downloaded.
+ */
+ public void addLocalFeedData(boolean downloadEpisodes) throws Exception {
+ if (localFeedDataAdded) throw new IllegalStateException("addLocalFeedData was called twice on the same instance");
+ if (!feedDataHosted) {
+ addHostedFeedData();
+ }
+
+ List<FeedItem> queue = new ArrayList<FeedItem>();
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ for (Feed feed : hostedFeeds) {
+ feed.setDownloaded(true);
+ if (feed.getImage() != null) {
+ FeedImage image = feed.getImage();
+ image.setFile_url(image.getDownload_url());
+ image.setDownloaded(true);
+ }
+ if (downloadEpisodes) {
+ for (FeedItem item : feed.getItems()) {
+ if (item.hasMedia()) {
+ FeedMedia media = item.getMedia();
+ int fileId = Integer.parseInt(StringUtils.substringAfter(media.getDownload_url(), "files/"));
+ media.setFile_url(server.accessFile(fileId).getAbsolutePath());
+ media.setDownloaded(true);
+ }
+ }
+ }
+
+ queue.add(feed.getItems().get(0));
+ feed.getItems().get(1).getMedia().setPlaybackCompletionDate(new Date());
+ }
+ adapter.setCompleteFeed(hostedFeeds.toArray(new Feed[hostedFeeds.size()]));
+ adapter.setQueue(queue);
+ adapter.close();
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
+ EventDistributor.getInstance().sendQueueUpdateBroadcast();
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java
new file mode 100644
index 000000000..6c5a350de
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java
@@ -0,0 +1,94 @@
+package de.test.antennapod.ui;
+
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import org.apache.http.HttpStatus;
+
+import java.io.File;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * Test for the UITestUtils. Makes sure that all URLs are reachable and that the class does not cause any crashes.
+ */
+public class UITestUtilsTest extends InstrumentationTestCase {
+
+ private UITestUtils uiTestUtils;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ uiTestUtils = new UITestUtils(getInstrumentation().getTargetContext());
+ uiTestUtils.setup();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ uiTestUtils.tearDown();
+ }
+
+ public void testAddHostedFeeds() throws Exception {
+ uiTestUtils.addHostedFeedData();
+ final List<Feed> feeds = uiTestUtils.hostedFeeds;
+ assertNotNull(feeds);
+ assertFalse(feeds.isEmpty());
+
+ for (Feed feed : feeds) {
+ testUrlReachable(feed.getDownload_url());
+ if (feed.getImage() != null) {
+ testUrlReachable(feed.getImage().getDownload_url());
+ }
+ for (FeedItem item : feed.getItems()) {
+ if (item.hasMedia()) {
+ testUrlReachable(item.getMedia().getDownload_url());
+ }
+ }
+ }
+ }
+
+ private void testUrlReachable(String strUtl) throws Exception {
+ URL url = new URL(strUtl);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.connect();
+ int rc = conn.getResponseCode();
+ assertEquals(HttpStatus.SC_OK, rc);
+ conn.disconnect();
+ }
+
+ private void addLocalFeedDataCheck(boolean downloadEpisodes) throws Exception {
+ uiTestUtils.addLocalFeedData(downloadEpisodes);
+ assertNotNull(uiTestUtils.hostedFeeds);
+ assertFalse(uiTestUtils.hostedFeeds.isEmpty());
+
+ for (Feed feed : uiTestUtils.hostedFeeds) {
+ assertTrue(feed.getId() != 0);
+ if (feed.getImage() != null) {
+ assertTrue(feed.getImage().getId() != 0);
+ }
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ if (item.hasMedia()) {
+ assertTrue(item.getMedia().getId() != 0);
+ if (downloadEpisodes) {
+ assertTrue(item.getMedia().isDownloaded());
+ assertNotNull(item.getMedia().getFile_url());
+ File file = new File(item.getMedia().getFile_url());
+ assertTrue(file.exists());
+ }
+ }
+ }
+ }
+ }
+
+ public void testAddLocalFeedDataNoDownload() throws Exception {
+ addLocalFeedDataCheck(false);
+ }
+
+ public void testAddLocalFeedDataDownload() throws Exception {
+ addLocalFeedDataCheck(true);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java
new file mode 100644
index 000000000..da6f07cab
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/ui/VideoplayerActivityTest.java
@@ -0,0 +1,38 @@
+package de.test.antennapod.ui;
+
+import android.test.ActivityInstrumentationTestCase2;
+
+import com.robotium.solo.Solo;
+
+import de.danoeh.antennapod.activity.VideoplayerActivity;
+
+/**
+ * Test class for VideoplayerActivity
+ */
+public class VideoplayerActivityTest extends ActivityInstrumentationTestCase2<VideoplayerActivity> {
+
+ private Solo solo;
+
+ public VideoplayerActivityTest() {
+ super(VideoplayerActivity.class);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ solo = new Solo(getInstrumentation(), getActivity());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ solo.finishOpenedActivities();
+ super.tearDown();
+ }
+
+ /**
+ * Test if activity can be started.
+ */
+ public void testStartActivity() throws Exception {
+ solo.waitForActivity(VideoplayerActivity.class);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java
new file mode 100644
index 000000000..47fca41ba
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java
@@ -0,0 +1,35 @@
+package de.test.antennapod.util;
+
+import android.test.AndroidTestCase;
+
+import de.danoeh.antennapod.core.util.Converter;
+
+/**
+ * Test class for converter
+ */
+public class ConverterTest extends AndroidTestCase {
+
+ public void testGetDurationStringLong() throws Exception {
+ String expected = "13:05:10";
+ int input = 47110000;
+ assertEquals(expected, Converter.getDurationStringLong(input));
+ }
+
+ public void testGetDurationStringShort() throws Exception {
+ String expected = "13:05";
+ int input = 47110000;
+ assertEquals(expected, Converter.getDurationStringShort(input));
+ }
+
+ public void testDurationStringLongToMs() throws Exception {
+ String input = "01:20:30";
+ long expected = 4830000;
+ assertEquals(expected, Converter.durationStringLongToMs(input));
+ }
+
+ public void testDurationStringShortToMs() throws Exception {
+ String input = "8:30";
+ long expected = 30600000;
+ assertEquals(expected, Converter.durationStringShortToMs(input));
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java b/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java
new file mode 100644
index 000000000..6d24fa526
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java
@@ -0,0 +1,59 @@
+package de.test.antennapod.util;
+
+import java.io.File;
+import java.io.IOException;
+
+import de.danoeh.antennapod.core.util.FileNameGenerator;
+import android.test.AndroidTestCase;
+
+public class FilenameGeneratorTest extends AndroidTestCase {
+
+ private static final String VALID1 = "abc abc";
+ private static final String INVALID1 = "ab/c: <abc";
+ private static final String INVALID2 = "abc abc ";
+
+ public FilenameGeneratorTest() {
+ super();
+ }
+
+ public void testGenerateFileName() throws IOException {
+ String result = FileNameGenerator.generateFileName(VALID1);
+ assertEquals(result, VALID1);
+ createFiles(result);
+ }
+
+ public void testGenerateFileName1() throws IOException {
+ String result = FileNameGenerator.generateFileName(INVALID1);
+ assertEquals(result, VALID1);
+ createFiles(result);
+ }
+
+ public void testGenerateFileName2() throws IOException {
+ String result = FileNameGenerator.generateFileName(INVALID2);
+ assertEquals(result, VALID1);
+ createFiles(result);
+ }
+
+ /**
+ * Tests if files can be created.
+ *
+ * @throws IOException
+ */
+ private void createFiles(String name) throws IOException {
+ File cache = getContext().getExternalCacheDir();
+ File testFile = new File(cache, name);
+ testFile.mkdir();
+ assertTrue(testFile.exists());
+ testFile.delete();
+ assertTrue(testFile.createNewFile());
+
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ File f = new File(getContext().getExternalCacheDir(), VALID1);
+ f.delete();
+ }
+
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java b/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java
new file mode 100644
index 000000000..7bdcfb898
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java
@@ -0,0 +1,21 @@
+package de.test.antennapod.util;
+
+import android.test.AndroidTestCase;
+import de.danoeh.antennapod.core.util.URIUtil;
+
+/**
+ * Test class for URIUtil
+ */
+public class URIUtilTest extends AndroidTestCase {
+
+ public void testGetURIFromRequestUrlShouldNotEncode() {
+ final String testUrl = "http://example.com/this%20is%20encoded";
+ assertEquals(testUrl, URIUtil.getURIFromRequestUrl(testUrl).toString());
+ }
+
+ public void testGetURIFromRequestUrlShouldEncode() {
+ final String testUrl = "http://example.com/this is not encoded";
+ final String expected = "http://example.com/this%20is%20not%20encoded";
+ assertEquals(expected, URIUtil.getURIFromRequestUrl(testUrl).toString());
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java b/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java
new file mode 100644
index 000000000..47b58268b
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java
@@ -0,0 +1,76 @@
+package de.test.antennapod.util;
+
+import android.test.AndroidTestCase;
+import de.danoeh.antennapod.core.util.URLChecker;
+
+/**
+ * Test class for URLChecker
+ */
+public class URLCheckerTest extends AndroidTestCase {
+
+ public void testCorrectURLHttp() {
+ final String in = "http://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals(in, out);
+ }
+
+ public void testCorrectURLHttps() {
+ final String in = "https://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals(in, out);
+ }
+
+ public void testMissingProtocol() {
+ final String in = "example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("http://example.com", out);
+ }
+
+ public void testFeedProtocol() {
+ final String in = "feed://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("http://example.com", out);
+ }
+
+ public void testPcastProtocolNoScheme() {
+ final String in = "pcast://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("http://example.com", out);
+ }
+
+ public void testItpcProtocol() {
+ final String in = "itpc://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("http://example.com", out);
+ }
+
+ public void testWhiteSpaceUrlShouldNotAppend() {
+ final String in = "\n http://example.com \t";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("http://example.com", out);
+ }
+
+ public void testWhiteSpaceShouldAppend() {
+ final String in = "\n example.com \t";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("http://example.com", out);
+ }
+
+ public void testAntennaPodSubscribeProtocolNoScheme() throws Exception {
+ final String in = "antennapod-subscribe://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("http://example.com", out);
+ }
+
+ public void testPcastProtocolWithScheme() {
+ final String in = "pcast://https://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("https://example.com", out);
+ }
+
+ public void testAntennaPodSubscribeProtocolWithScheme() throws Exception {
+ final String in = "antennapod-subscribe://https://example.com";
+ final String out = URLChecker.prepareURL(in);
+ assertEquals("https://example.com", out);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java
new file mode 100644
index 000000000..2c56b71cc
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java
@@ -0,0 +1,127 @@
+package de.test.antennapod.util.playback;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.util.Date;
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.Timeline;
+
+/**
+ * Test class for timeline
+ */
+public class TimelineTest extends InstrumentationTestCase {
+
+ private Context context;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ context = getInstrumentation().getTargetContext();
+ }
+
+ private Playable newTestPlayable(List<Chapter> chapters, String shownotes) {
+ FeedItem item = new FeedItem(0, "Item", "item-id", "http://example.com/item", new Date(), true, null);
+ item.setChapters(chapters);
+ item.setContentEncoded(shownotes);
+ FeedMedia media = new FeedMedia(item, "http://example.com/episode", 100, "audio/mp3");
+ media.setDuration(Integer.MAX_VALUE);
+ item.setMedia(media);
+ return media;
+ }
+
+ public void testProcessShownotesAddTimecodeHHMMSSNoChapters() throws Exception {
+ final String timeStr = "10:11:12";
+ final long time = 3600 * 1000 * 10 + 60 * 1000 * 11 + 12 * 1000;
+
+ Playable p = newTestPlayable(null, "<p> Some test text with a timecode " + timeStr + " here.</p>");
+ Timeline t = new Timeline(context, p);
+ String res = t.processShownotes(true);
+ checkLinkCorrect(res, new long[]{time}, new String[]{timeStr});
+ }
+
+ public void testProcessShownotesAddTimecodeHHMMNoChapters() throws Exception {
+ final String timeStr = "10:11";
+ final long time = 3600 * 1000 * 10 + 60 * 1000 * 11;
+
+ Playable p = newTestPlayable(null, "<p> Some test text with a timecode " + timeStr + " here.</p>");
+ Timeline t = new Timeline(context, p);
+ String res = t.processShownotes(true);
+ checkLinkCorrect(res, new long[]{time}, new String[]{timeStr});
+ }
+
+ public void testProcessShownotesAddTimecodeParentheses() throws Exception {
+ final String timeStr = "10:11";
+ final long time = 3600 * 1000 * 10 + 60 * 1000 * 11;
+
+ Playable p = newTestPlayable(null, "<p> Some test text with a timecode (" + timeStr + ") here.</p>");
+ Timeline t = new Timeline(context, p);
+ String res = t.processShownotes(true);
+ checkLinkCorrect(res, new long[]{time}, new String[]{timeStr});
+ }
+
+ public void testProcessShownotesAddTimecodeBrackets() throws Exception {
+ final String timeStr = "10:11";
+ final long time = 3600 * 1000 * 10 + 60 * 1000 * 11;
+
+ Playable p = newTestPlayable(null, "<p> Some test text with a timecode [" + timeStr + "] here.</p>");
+ Timeline t = new Timeline(context, p);
+ String res = t.processShownotes(true);
+ checkLinkCorrect(res, new long[]{time}, new String[]{timeStr});
+ }
+
+ public void testProcessShownotesAddTimecodeAngleBrackets() throws Exception {
+ final String timeStr = "10:11";
+ final long time = 3600 * 1000 * 10 + 60 * 1000 * 11;
+
+ Playable p = newTestPlayable(null, "<p> Some test text with a timecode <" + timeStr + "> here.</p>");
+ Timeline t = new Timeline(context, p);
+ String res = t.processShownotes(true);
+ checkLinkCorrect(res, new long[]{time}, new String[]{timeStr});
+ }
+
+ private void checkLinkCorrect(String res, long[] timecodes, String[] timecodeStr) {
+ assertNotNull(res);
+ Document d = Jsoup.parse(res);
+ Elements links = d.body().getElementsByTag("a");
+ int countedLinks = 0;
+ for (Element link : links) {
+ String href = link.attributes().get("href");
+ String text = link.text();
+ if (href.startsWith("antennapod://")) {
+ assertTrue(href.endsWith(String.valueOf(timecodes[countedLinks])));
+ assertEquals(timecodeStr[countedLinks], text);
+ countedLinks++;
+ assertTrue("Contains too many links: " + countedLinks + " > " + timecodes.length, countedLinks <= timecodes.length);
+ }
+ }
+ assertEquals(timecodes.length, countedLinks);
+ }
+
+ public void testIsTimecodeLink() throws Exception {
+ assertFalse(Timeline.isTimecodeLink(null));
+ assertFalse(Timeline.isTimecodeLink("http://antennapod/timecode/123123"));
+ assertFalse(Timeline.isTimecodeLink("antennapod://timecode/"));
+ assertFalse(Timeline.isTimecodeLink("antennapod://123123"));
+ assertFalse(Timeline.isTimecodeLink("antennapod://timecode/123123a"));
+ assertTrue(Timeline.isTimecodeLink("antennapod://timecode/123"));
+ assertTrue(Timeline.isTimecodeLink("antennapod://timecode/1"));
+ }
+
+ public void testGetTimecodeLinkTime() throws Exception {
+ assertEquals(-1, Timeline.getTimecodeLinkTime(null));
+ assertEquals(-1, Timeline.getTimecodeLinkTime("http://timecode/123"));
+ assertEquals(123, Timeline.getTimecodeLinkTime("antennapod://timecode/123"));
+
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java
new file mode 100644
index 000000000..5cb723446
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java
@@ -0,0 +1,346 @@
+package de.test.antennapod.util.service.download;
+
+import android.util.Base64;
+import android.util.Log;
+import de.danoeh.antennapod.BuildConfig;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.*;
+import java.net.URLConnection;
+import java.util.*;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Http server for testing purposes
+ * <p/>
+ * Supported features:
+ * <p/>
+ * /status/code: Returns HTTP response with the given status code
+ * /redirect/n: Redirects n times
+ * /delay/n: Delay response for n seconds
+ * /basic-auth/username/password: Basic auth with username and password
+ * /gzip/n: Send gzipped data of size n bytes
+ * /files/id: Accesses the file with the specified ID (this has to be added first via serveFile).
+ */
+public class HTTPBin extends NanoHTTPD {
+ private static final String TAG = "HTTPBin";
+ public static final int PORT = 8124;
+ public static final String BASE_URL = "http://127.0.0.1:" + HTTPBin.PORT;
+
+
+ private static final String MIME_HTML = "text/html";
+ private static final String MIME_PLAIN = "text/plain";
+
+ private List<File> servedFiles;
+
+ public HTTPBin() {
+ super(PORT);
+ this.servedFiles = new ArrayList<File>();
+ }
+
+ /**
+ * Adds the given file to the server.
+ *
+ * @return The ID of the file or -1 if the file could not be added to the server.
+ */
+ public synchronized int serveFile(File file) {
+ if (file == null) throw new IllegalArgumentException("file = null");
+ if (!file.exists()) {
+ return -1;
+ }
+ for (int i = 0; i < servedFiles.size(); i++) {
+ if (servedFiles.get(i).getAbsolutePath().equals(file.getAbsolutePath())) {
+ return i;
+ }
+ }
+ servedFiles.add(file);
+ return servedFiles.size() - 1;
+ }
+
+ /**
+ * Removes the file with the given ID from the server.
+ *
+ * @return True if a file was removed, false otherwise
+ */
+ public synchronized boolean removeFile(int id) {
+ if (id < 0) throw new IllegalArgumentException("ID < 0");
+ if (id >= servedFiles.size()) {
+ return false;
+ } else {
+ return servedFiles.remove(id) != null;
+ }
+ }
+
+ public synchronized File accessFile(int id) {
+ if (id < 0 || id >= servedFiles.size()) {
+ return null;
+ } else {
+ return servedFiles.get(id);
+ }
+ }
+
+ @Override
+ public Response serve(IHTTPSession session) {
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Requested url: " + session.getUri());
+
+ String[] segments = session.getUri().split("/");
+ if (segments.length < 3) {
+ Log.w(TAG, String.format("Invalid number of URI segments: %d %s", segments.length, Arrays.toString(segments)));
+ get404Error();
+ }
+
+ final String func = segments[1];
+ final String param = segments[2];
+ final Map<String, String> headers = session.getHeaders();
+
+ if (func.equalsIgnoreCase("status")) {
+ try {
+ int code = Integer.parseInt(param);
+ return getStatus(code);
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ return getInternalError();
+ }
+
+ } else if (func.equalsIgnoreCase("redirect")) {
+ try {
+ int times = Integer.parseInt(param);
+ if (times < 0) {
+ throw new NumberFormatException("times <= 0: " + times);
+ }
+
+ return getRedirectResponse(times - 1);
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ return getInternalError();
+ }
+ } else if (func.equalsIgnoreCase("delay")) {
+ try {
+ int sec = Integer.parseInt(param);
+ if (sec <= 0) {
+ throw new NumberFormatException("sec <= 0: " + sec);
+ }
+
+ Thread.sleep(sec * 1000L);
+ return getOKResponse();
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ return getInternalError();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ return getInternalError();
+ }
+ } else if (func.equalsIgnoreCase("basic-auth")) {
+ if (!headers.containsKey("authorization")) {
+ Log.w(TAG, "No credentials provided");
+ return getUnauthorizedResponse();
+ }
+ try {
+ String credentials = new String(Base64.decode(headers.get("authorization").split(" ")[1], 0), "UTF-8");
+ String[] credentialParts = credentials.split(":");
+ if (credentialParts.length != 2) {
+ Log.w(TAG, "Unable to split credentials: " + Arrays.toString(credentialParts));
+ return getInternalError();
+ }
+ if (credentialParts[0].equals(segments[2])
+ && credentialParts[1].equals(segments[3])) {
+ Log.i(TAG, "Credentials accepted");
+ return getOKResponse();
+ } else {
+ Log.w(TAG, String.format("Invalid credentials. Expected %s, %s, but was %s, %s",
+ segments[2], segments[3], credentialParts[0], credentialParts[1]));
+ return getUnauthorizedResponse();
+ }
+
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ return getInternalError();
+ }
+ } else if (func.equalsIgnoreCase("gzip")) {
+ try {
+ int size = Integer.parseInt(param);
+ if (size <= 0) {
+ Log.w(TAG, "Invalid size for gzipped data: " + size);
+ throw new NumberFormatException();
+ }
+
+ return getGzippedResponse(size);
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ return getInternalError();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return getInternalError();
+ }
+ } else if (func.equalsIgnoreCase("files")) {
+ try {
+ int id = Integer.parseInt(param);
+ if (id < 0) {
+ Log.w(TAG, "Invalid ID: " + id);
+ throw new NumberFormatException();
+ }
+ return getFileAccessResponse(id, headers);
+
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ return getInternalError();
+ }
+ }
+
+ return get404Error();
+ }
+
+ private synchronized Response getFileAccessResponse(int id, Map<String, String> header) {
+ File file = accessFile(id);
+ if (file == null || !file.exists()) {
+ Log.w(TAG, "File not found: " + id);
+ return get404Error();
+ }
+ InputStream inputStream = null;
+ String contentRange = null;
+ Response.Status status;
+ boolean successful = false;
+ try {
+ inputStream = new FileInputStream(file);
+ if (header.containsKey("range")) {
+ // read range header field
+ final String value = header.get("range");
+ final String[] segments = value.split("=");
+ if (segments.length != 2) {
+ Log.w(TAG, "Invalid segment length: " + Arrays.toString(segments));
+ return getInternalError();
+ }
+ final String type = StringUtils.substringBefore(value, "=");
+ if (!type.equalsIgnoreCase("bytes")) {
+ Log.w(TAG, "Range is not specified in bytes: " + value);
+ return getInternalError();
+ }
+ try {
+ long start = Long.parseLong(StringUtils.substringBefore(segments[1], "-"));
+ if (start >= file.length()) {
+ return getRangeNotSatisfiable();
+ }
+
+ // skip 'start' bytes
+ IOUtils.skipFully(inputStream, start);
+ contentRange = "bytes " + start + (file.length() - 1) + "/" + file.length();
+
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ return getInternalError();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return getInternalError();
+ }
+
+ status = Response.Status.PARTIAL_CONTENT;
+
+ } else {
+ // request did not contain range header field
+ status = Response.Status.OK;
+ }
+ successful = true;
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+
+ return getInternalError();
+ } finally {
+ if (!successful && inputStream != null) {
+ IOUtils.closeQuietly(inputStream);
+ }
+ }
+
+ Response response = new Response(status, URLConnection.guessContentTypeFromName(file.getAbsolutePath()), inputStream);
+
+ response.addHeader("Accept-Ranges", "bytes");
+ if (contentRange != null) {
+ response.addHeader("Content-Range", contentRange);
+ }
+ response.addHeader("Content-Length", String.valueOf(file.length()));
+ return response;
+ }
+
+ private Response getGzippedResponse(int size) throws IOException {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ final byte[] buffer = new byte[size];
+ Random random = new Random(System.currentTimeMillis());
+ random.nextBytes(buffer);
+
+ ByteArrayOutputStream compressed = new ByteArrayOutputStream();
+ GZIPOutputStream gzipOutputStream = new GZIPOutputStream(compressed);
+ gzipOutputStream.write(buffer);
+
+ InputStream inputStream = new ByteArrayInputStream(compressed.toByteArray());
+ Response response = new Response(Response.Status.OK, MIME_PLAIN, inputStream);
+ response.addHeader("Content-encoding", "gzip");
+ response.addHeader("Content-length", String.valueOf(compressed.size()));
+ return response;
+ }
+
+ private Response getStatus(final int code) {
+ Response.IStatus status = (code == 200) ? Response.Status.OK :
+ (code == 201) ? Response.Status.CREATED :
+ (code == 206) ? Response.Status.PARTIAL_CONTENT :
+ (code == 301) ? Response.Status.REDIRECT :
+ (code == 304) ? Response.Status.NOT_MODIFIED :
+ (code == 400) ? Response.Status.BAD_REQUEST :
+ (code == 401) ? Response.Status.UNAUTHORIZED :
+ (code == 403) ? Response.Status.FORBIDDEN :
+ (code == 404) ? Response.Status.NOT_FOUND :
+ (code == 405) ? Response.Status.METHOD_NOT_ALLOWED :
+ (code == 416) ? Response.Status.RANGE_NOT_SATISFIABLE :
+ (code == 500) ? Response.Status.INTERNAL_ERROR : new Response.IStatus() {
+ @Override
+ public int getRequestStatus() {
+ return code;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Unknown";
+ }
+ };
+ return new Response(status, MIME_HTML, "");
+
+ }
+
+ private Response getRedirectResponse(int times) {
+ if (times > 0) {
+ Response response = new Response(Response.Status.REDIRECT, MIME_HTML, "This resource has been moved permanently");
+ response.addHeader("Location", "/redirect/" + times);
+ return response;
+ } else if (times == 0) {
+ return getOKResponse();
+ } else {
+ return getInternalError();
+ }
+ }
+
+ private Response getUnauthorizedResponse() {
+ Response response = new Response(Response.Status.UNAUTHORIZED, MIME_HTML, "");
+ response.addHeader("WWW-Authenticate", "Basic realm=\"Test Realm\"");
+ return response;
+ }
+
+ private Response getOKResponse() {
+ return new Response(Response.Status.OK, MIME_HTML, "");
+ }
+
+ private Response getInternalError() {
+ return new Response(Response.Status.INTERNAL_ERROR, MIME_HTML, "The server encountered an internal error");
+ }
+
+ private Response getRangeNotSatisfiable() {
+ return new Response(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAIN, "");
+ }
+
+ private Response get404Error() {
+ return new Response(Response.Status.NOT_FOUND, MIME_HTML, "The requested URL was not found on this server");
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/service/download/NanoHTTPD.java b/app/src/androidTest/java/de/test/antennapod/util/service/download/NanoHTTPD.java
new file mode 100644
index 000000000..4a5818479
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/service/download/NanoHTTPD.java
@@ -0,0 +1,1420 @@
+package de.test.antennapod.util.service.download;
+
+import java.io.*;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URLDecoder;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+
+/**
+ * A simple, tiny, nicely embeddable HTTP server in Java
+ * <p/>
+ * <p/>
+ * NanoHTTPD
+ * <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p>
+ * <p/>
+ * <p/>
+ * <b>Features + limitations: </b>
+ * <ul>
+ * <p/>
+ * <li>Only one Java file</li>
+ * <li>Java 5 compatible</li>
+ * <li>Released as open source, Modified BSD licence</li>
+ * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li>
+ * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li>
+ * <li>Supports both dynamic content and file serving</li>
+ * <li>Supports file upload (since version 1.2, 2010)</li>
+ * <li>Supports partial content (streaming)</li>
+ * <li>Supports ETags</li>
+ * <li>Never caches anything</li>
+ * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
+ * <li>Default code serves files and shows all HTTP parameters and headers</li>
+ * <li>File server supports directory listing, index.html and index.htm</li>
+ * <li>File server supports partial content (streaming)</li>
+ * <li>File server supports ETags</li>
+ * <li>File server does the 301 redirection trick for directories without '/'</li>
+ * <li>File server supports simple skipping for files (continue download)</li>
+ * <li>File server serves also very long files without memory overhead</li>
+ * <li>Contains a built-in list of most common mime types</li>
+ * <li>All header names are converted lowercase so they don't vary between browsers/clients</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * <p/>
+ * <b>How to use: </b>
+ * <ul>
+ * <p/>
+ * <li>Subclass and implement serve() and embed to your own program</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence)
+ */
+public abstract class NanoHTTPD {
+ /**
+ * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
+ * This is required as the Keep-Alive HTTP connections would otherwise
+ * block the socket reading thread forever (or as long the browser is open).
+ */
+ public static final int SOCKET_READ_TIMEOUT = 5000;
+ /**
+ * Common mime type for dynamic content: plain text
+ */
+ public static final String MIME_PLAINTEXT = "text/plain";
+ /**
+ * Common mime type for dynamic content: html
+ */
+ public static final String MIME_HTML = "text/html";
+ /**
+ * Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing.
+ */
+ private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
+ private final String hostname;
+ private final int myPort;
+ private ServerSocket myServerSocket;
+ private Set<Socket> openConnections = new HashSet<Socket>();
+ private Thread myThread;
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ private AsyncRunner asyncRunner;
+ /**
+ * Pluggable strategy for creating and cleaning up temporary files.
+ */
+ private TempFileManagerFactory tempFileManagerFactory;
+
+ /**
+ * Constructs an HTTP server on given port.
+ */
+ public NanoHTTPD(int port) {
+ this(null, port);
+ }
+
+ /**
+ * Constructs an HTTP server on given hostname and port.
+ */
+ public NanoHTTPD(String hostname, int port) {
+ this.hostname = hostname;
+ this.myPort = port;
+ setTempFileManagerFactory(new DefaultTempFileManagerFactory());
+ setAsyncRunner(new DefaultAsyncRunner());
+ }
+
+ private static final void safeClose(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ private static final void safeClose(Socket closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ private static final void safeClose(ServerSocket closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ /**
+ * Start the server.
+ *
+ * @throws IOException if the socket is in use.
+ */
+ public void start() throws IOException {
+ myServerSocket = new ServerSocket();
+ myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
+
+ myThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ do {
+ try {
+ final Socket finalAccept = myServerSocket.accept();
+ registerConnection(finalAccept);
+ finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
+ final InputStream inputStream = finalAccept.getInputStream();
+ asyncRunner.exec(new Runnable() {
+ @Override
+ public void run() {
+ OutputStream outputStream = null;
+ try {
+ outputStream = finalAccept.getOutputStream();
+ TempFileManager tempFileManager = tempFileManagerFactory.create();
+ HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());
+ while (!finalAccept.isClosed()) {
+ session.execute();
+ }
+ } catch (Exception e) {
+ // When the socket is closed by the client, we throw our own SocketException
+ // to break the "keep alive" loop above.
+ if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) {
+ e.printStackTrace();
+ }
+ } finally {
+ safeClose(outputStream);
+ safeClose(inputStream);
+ safeClose(finalAccept);
+ unRegisterConnection(finalAccept);
+ }
+ }
+ });
+ } catch (IOException e) {
+ }
+ } while (!myServerSocket.isClosed());
+ }
+ });
+ myThread.setDaemon(true);
+ myThread.setName("NanoHttpd Main Listener");
+ myThread.start();
+ }
+
+ /**
+ * Stop the server.
+ */
+ public void stop() {
+ try {
+ safeClose(myServerSocket);
+ closeAllConnections();
+ if (myThread != null) {
+ myThread.join();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Registers that a new connection has been set up.
+ *
+ * @param socket the {@link Socket} for the connection.
+ */
+ public synchronized void registerConnection(Socket socket) {
+ openConnections.add(socket);
+ }
+
+ /**
+ * Registers that a connection has been closed
+ *
+ * @param socket
+ * the {@link Socket} for the connection.
+ */
+ public synchronized void unRegisterConnection(Socket socket) {
+ openConnections.remove(socket);
+ }
+
+ /**
+ * Forcibly closes all connections that are open.
+ */
+ public synchronized void closeAllConnections() {
+ for (Socket socket : openConnections) {
+ safeClose(socket);
+ }
+ }
+
+ public final int getListeningPort() {
+ return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
+ }
+
+ public final boolean wasStarted() {
+ return myServerSocket != null && myThread != null;
+ }
+
+ public final boolean isAlive() {
+ return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive();
+ }
+
+ /**
+ * Override this to customize the server.
+ * <p/>
+ * <p/>
+ * (By default, this delegates to serveFile() and allows directory listing.)
+ *
+ * @param uri Percent-decoded URI without parameters, for example "/index.cgi"
+ * @param method "GET", "POST" etc.
+ * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data.
+ * @param headers Header entries, percent decoded
+ * @return HTTP response, see class Response for details
+ */
+ @Deprecated
+ public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms,
+ Map<String, String> files) {
+ return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found");
+ }
+
+ /**
+ * Override this to customize the server.
+ * <p/>
+ * <p/>
+ * (By default, this delegates to serveFile() and allows directory listing.)
+ *
+ * @param session The HTTP session
+ * @return HTTP response, see class Response for details
+ */
+ public Response serve(IHTTPSession session) {
+ Map<String, String> files = new HashMap<String, String>();
+ Method method = session.getMethod();
+ if (Method.PUT.equals(method) || Method.POST.equals(method)) {
+ try {
+ session.parseBody(files);
+ } catch (IOException ioe) {
+ return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+ } catch (ResponseException re) {
+ return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
+ }
+ }
+
+ Map<String, String> parms = session.getParms();
+ parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());
+ return serve(session.getUri(), method, session.getHeaders(), parms, files);
+ }
+
+ /**
+ * Decode percent encoded <code>String</code> values.
+ *
+ * @param str the percent encoded <code>String</code>
+ * @return expanded form of the input, for example "foo%20bar" becomes "foo bar"
+ */
+ protected String decodePercent(String str) {
+ String decoded = null;
+ try {
+ decoded = URLDecoder.decode(str, "UTF8");
+ } catch (UnsupportedEncodingException ignored) {
+ }
+ return decoded;
+ }
+
+ /**
+ * Decode parameters from a URL, handing the case where a single parameter name might have been
+ * supplied several times, by return lists of values. In general these lists will contain a single
+ * element.
+ *
+ * @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method.
+ * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a list of the values supplied).
+ */
+ protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
+ return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER));
+ }
+
+ /**
+ * Decode parameters from a URL, handing the case where a single parameter name might have been
+ * supplied several times, by return lists of values. In general these lists will contain a single
+ * element.
+ *
+ * @param queryString a query string pulled from the URL.
+ * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a list of the values supplied).
+ */
+ protected Map<String, List<String>> decodeParameters(String queryString) {
+ Map<String, List<String>> parms = new HashMap<String, List<String>>();
+ if (queryString != null) {
+ StringTokenizer st = new StringTokenizer(queryString, "&");
+ while (st.hasMoreTokens()) {
+ String e = st.nextToken();
+ int sep = e.indexOf('=');
+ String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
+ if (!parms.containsKey(propertyName)) {
+ parms.put(propertyName, new ArrayList<String>());
+ }
+ String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null;
+ if (propertyValue != null) {
+ parms.get(propertyName).add(propertyValue);
+ }
+ }
+ }
+ return parms;
+ }
+
+ // ------------------------------------------------------------------------------- //
+ //
+ // Threading Strategy.
+ //
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ *
+ * @param asyncRunner new strategy for handling threads.
+ */
+ public void setAsyncRunner(AsyncRunner asyncRunner) {
+ this.asyncRunner = asyncRunner;
+ }
+
+ // ------------------------------------------------------------------------------- //
+ //
+ // Temp file handling strategy.
+ //
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * Pluggable strategy for creating and cleaning up temporary files.
+ *
+ * @param tempFileManagerFactory new strategy for handling temp files.
+ */
+ public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
+ this.tempFileManagerFactory = tempFileManagerFactory;
+ }
+
+ /**
+ * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value.
+ */
+ public enum Method {
+ GET, PUT, POST, DELETE, HEAD, OPTIONS;
+
+ static Method lookup(String method) {
+ for (Method m : Method.values()) {
+ if (m.toString().equalsIgnoreCase(method)) {
+ return m;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ public interface AsyncRunner {
+ void exec(Runnable code);
+ }
+
+ /**
+ * Factory to create temp file managers.
+ */
+ public interface TempFileManagerFactory {
+ TempFileManager create();
+ }
+
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * Temp file manager.
+ * <p/>
+ * <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup
+ * temporary files created as a result of handling the request.</p>
+ */
+ public interface TempFileManager {
+ TempFile createTempFile() throws Exception;
+
+ void clear();
+ }
+
+ /**
+ * A temp file.
+ * <p/>
+ * <p>Temp files are responsible for managing the actual temporary storage and cleaning
+ * themselves up when no longer needed.</p>
+ */
+ public interface TempFile {
+ OutputStream open() throws Exception;
+
+ void delete() throws Exception;
+
+ String getName();
+ }
+
+ /**
+ * Default threading strategy for NanoHttpd.
+ * <p/>
+ * <p>By default, the server spawns a new Thread for every incoming request. These are set
+ * to <i>daemon</i> status, and named according to the request number. The name is
+ * useful when profiling the application.</p>
+ */
+ public static class DefaultAsyncRunner implements AsyncRunner {
+ private long requestCount;
+
+ @Override
+ public void exec(Runnable code) {
+ ++requestCount;
+ Thread t = new Thread(code);
+ t.setDaemon(true);
+ t.setName("NanoHttpd Request Processor (#" + requestCount + ")");
+ t.start();
+ }
+ }
+
+ /**
+ * Default strategy for creating and cleaning up temporary files.
+ * <p/>
+ * <p></p>This class stores its files in the standard location (that is,
+ * wherever <code>java.io.tmpdir</code> points to). Files are added
+ * to an internal list, and deleted when no longer needed (that is,
+ * when <code>clear()</code> is invoked at the end of processing a
+ * request).</p>
+ */
+ public static class DefaultTempFileManager implements TempFileManager {
+ private final String tmpdir;
+ private final List<TempFile> tempFiles;
+
+ public DefaultTempFileManager() {
+ tmpdir = System.getProperty("java.io.tmpdir");
+ tempFiles = new ArrayList<TempFile>();
+ }
+
+ @Override
+ public TempFile createTempFile() throws Exception {
+ DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
+ tempFiles.add(tempFile);
+ return tempFile;
+ }
+
+ @Override
+ public void clear() {
+ for (TempFile file : tempFiles) {
+ try {
+ file.delete();
+ } catch (Exception ignored) {
+ }
+ }
+ tempFiles.clear();
+ }
+ }
+
+ /**
+ * Default strategy for creating and cleaning up temporary files.
+ * <p/>
+ * <p></p></[>By default, files are created by <code>File.createTempFile()</code> in
+ * the directory specified.</p>
+ */
+ public static class DefaultTempFile implements TempFile {
+ private File file;
+ private OutputStream fstream;
+
+ public DefaultTempFile(String tempdir) throws IOException {
+ file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
+ fstream = new FileOutputStream(file);
+ }
+
+ @Override
+ public OutputStream open() throws Exception {
+ return fstream;
+ }
+
+ @Override
+ public void delete() throws Exception {
+ safeClose(fstream);
+ file.delete();
+ }
+
+ @Override
+ public String getName() {
+ return file.getAbsolutePath();
+ }
+ }
+
+ /**
+ * HTTP response. Return one of these from serve().
+ */
+ public static class Response {
+ /**
+ * HTTP status code after processing, e.g. "200 OK", HTTP_OK
+ */
+ private IStatus status;
+ /**
+ * MIME type of content, e.g. "text/html"
+ */
+ private String mimeType;
+ /**
+ * Data of the response, may be null.
+ */
+ private InputStream data;
+ /**
+ * Headers for the HTTP response. Use addHeader() to add lines.
+ */
+ private Map<String, String> header = new HashMap<String, String>();
+ /**
+ * The request method that spawned this response.
+ */
+ private Method requestMethod;
+ /**
+ * Use chunkedTransfer
+ */
+ private boolean chunkedTransfer;
+
+ /**
+ * Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message
+ */
+ public Response(String msg) {
+ this(Status.OK, MIME_HTML, msg);
+ }
+
+ /**
+ * Basic constructor.
+ */
+ public Response(IStatus status, String mimeType, InputStream data) {
+ this.status = status;
+ this.mimeType = mimeType;
+ this.data = data;
+ }
+
+ /**
+ * Convenience method that makes an InputStream out of given text.
+ */
+ public Response(IStatus status, String mimeType, String txt) {
+ this.status = status;
+ this.mimeType = mimeType;
+ try {
+ this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null;
+ } catch (java.io.UnsupportedEncodingException uee) {
+ uee.printStackTrace();
+ }
+ }
+
+ /**
+ * Adds given line to the header.
+ */
+ public void addHeader(String name, String value) {
+ header.put(name, value);
+ }
+
+ public String getHeader(String name) {
+ return header.get(name);
+ }
+
+ /**
+ * Sends given response to the socket.
+ */
+ protected void send(OutputStream outputStream) {
+ String mime = mimeType;
+ SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+ gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
+
+ try {
+ if (status == null) {
+ throw new Error("sendResponse(): Status can't be null.");
+ }
+ PrintWriter pw = new PrintWriter(outputStream);
+ pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");
+
+ if (mime != null) {
+ pw.print("Content-Type: " + mime + "\r\n");
+ }
+
+ if (header == null || header.get("Date") == null) {
+ pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
+ }
+
+ if (header != null) {
+ for (String key : header.keySet()) {
+ String value = header.get(key);
+ pw.print(key + ": " + value + "\r\n");
+ }
+ }
+
+ sendConnectionHeaderIfNotAlreadyPresent(pw, header);
+
+ if (requestMethod != Method.HEAD && chunkedTransfer) {
+ sendAsChunked(outputStream, pw);
+ } else {
+ int pending = data != null ? data.available() : 0;
+ sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);
+ pw.print("\r\n");
+ pw.flush();
+ sendAsFixedLength(outputStream, pending);
+ }
+ outputStream.flush();
+ safeClose(data);
+ } catch (IOException ioe) {
+ // Couldn't write? No can do.
+ }
+ }
+
+ protected void sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, int size) {
+ if (!headerAlreadySent(header, "content-length")) {
+ pw.print("Content-Length: "+ size +"\r\n");
+ }
+ }
+
+ protected void sendConnectionHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header) {
+ if (!headerAlreadySent(header, "connection")) {
+ pw.print("Connection: keep-alive\r\n");
+ }
+ }
+
+ private boolean headerAlreadySent(Map<String, String> header, String name) {
+ boolean alreadySent = false;
+ for (String headerName : header.keySet()) {
+ alreadySent |= headerName.equalsIgnoreCase(name);
+ }
+ return alreadySent;
+ }
+
+ private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
+ pw.print("Transfer-Encoding: chunked\r\n");
+ pw.print("\r\n");
+ pw.flush();
+ int BUFFER_SIZE = 16 * 1024;
+ byte[] CRLF = "\r\n".getBytes();
+ byte[] buff = new byte[BUFFER_SIZE];
+ int read;
+ while ((read = data.read(buff)) > 0) {
+ outputStream.write(String.format("%x\r\n", read).getBytes());
+ outputStream.write(buff, 0, read);
+ outputStream.write(CRLF);
+ }
+ outputStream.write(String.format("0\r\n\r\n").getBytes());
+ }
+
+ private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {
+ if (requestMethod != Method.HEAD && data != null) {
+ int BUFFER_SIZE = 16 * 1024;
+ byte[] buff = new byte[BUFFER_SIZE];
+ while (pending > 0) {
+ int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
+ if (read <= 0) {
+ break;
+ }
+ outputStream.write(buff, 0, read);
+ pending -= read;
+ }
+ }
+ }
+
+ public IStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ public void setMimeType(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ public InputStream getData() {
+ return data;
+ }
+
+ public void setData(InputStream data) {
+ this.data = data;
+ }
+
+ public Method getRequestMethod() {
+ return requestMethod;
+ }
+
+ public void setRequestMethod(Method requestMethod) {
+ this.requestMethod = requestMethod;
+ }
+
+ public void setChunkedTransfer(boolean chunkedTransfer) {
+ this.chunkedTransfer = chunkedTransfer;
+ }
+
+ public interface IStatus {
+ int getRequestStatus();
+ String getDescription();
+ }
+
+ /**
+ * Some HTTP response status codes
+ */
+ public enum Status implements IStatus {
+ SWITCH_PROTOCOL(101, "Switching Protocols"), OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301,
+ "Moved Permanently"), NOT_MODIFIED(304, "Not Modified"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401,
+ "Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"), RANGE_NOT_SATISFIABLE(416,
+ "Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error");
+ private final int requestStatus;
+ private final String description;
+
+ Status(int requestStatus, String description) {
+ this.requestStatus = requestStatus;
+ this.description = description;
+ }
+
+ @Override
+ public int getRequestStatus() {
+ return this.requestStatus;
+ }
+
+ @Override
+ public String getDescription() {
+ return "" + this.requestStatus + " " + description;
+ }
+ }
+ }
+
+ public static final class ResponseException extends Exception {
+
+ private final Response.Status status;
+
+ public ResponseException(Response.Status status, String message) {
+ super(message);
+ this.status = status;
+ }
+
+ public ResponseException(Response.Status status, String message, Exception e) {
+ super(message, e);
+ this.status = status;
+ }
+
+ public Response.Status getStatus() {
+ return status;
+ }
+ }
+
+ /**
+ * Default strategy for creating and cleaning up temporary files.
+ */
+ private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
+ @Override
+ public TempFileManager create() {
+ return new DefaultTempFileManager();
+ }
+ }
+
+ /**
+ * Handles one session, i.e. parses the HTTP request and returns the response.
+ */
+ public interface IHTTPSession {
+ void execute() throws IOException;
+
+ Map<String, String> getParms();
+
+ Map<String, String> getHeaders();
+
+ /**
+ * @return the path part of the URL.
+ */
+ String getUri();
+
+ String getQueryParameterString();
+
+ Method getMethod();
+
+ InputStream getInputStream();
+
+ CookieHandler getCookies();
+
+ /**
+ * Adds the files in the request body to the files map.
+ * @arg files - map to modify
+ */
+ void parseBody(Map<String, String> files) throws IOException, ResponseException;
+ }
+
+ protected class HTTPSession implements IHTTPSession {
+ public static final int BUFSIZE = 8192;
+ private final TempFileManager tempFileManager;
+ private final OutputStream outputStream;
+ private PushbackInputStream inputStream;
+ private int splitbyte;
+ private int rlen;
+ private String uri;
+ private Method method;
+ private Map<String, String> parms;
+ private Map<String, String> headers;
+ private CookieHandler cookies;
+ private String queryParameterString;
+
+ public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
+ this.tempFileManager = tempFileManager;
+ this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
+ this.outputStream = outputStream;
+ }
+
+ public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
+ this.tempFileManager = tempFileManager;
+ this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
+ this.outputStream = outputStream;
+ String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
+ headers = new HashMap<String, String>();
+
+ headers.put("remote-addr", remoteIp);
+ headers.put("http-client-ip", remoteIp);
+ }
+
+ @Override
+ public void execute() throws IOException {
+ try {
+ // Read the first 8192 bytes.
+ // The full header should fit in here.
+ // Apache's default header limit is 8KB.
+ // Do NOT assume that a single read will get the entire header at once!
+ byte[] buf = new byte[BUFSIZE];
+ splitbyte = 0;
+ rlen = 0;
+ {
+ int read = -1;
+ try {
+ read = inputStream.read(buf, 0, BUFSIZE);
+ } catch (Exception e) {
+ safeClose(inputStream);
+ safeClose(outputStream);
+ throw new SocketException("NanoHttpd Shutdown");
+ }
+ if (read == -1) {
+ // socket was been closed
+ safeClose(inputStream);
+ safeClose(outputStream);
+ throw new SocketException("NanoHttpd Shutdown");
+ }
+ while (read > 0) {
+ rlen += read;
+ splitbyte = findHeaderEnd(buf, rlen);
+ if (splitbyte > 0)
+ break;
+ read = inputStream.read(buf, rlen, BUFSIZE - rlen);
+ }
+ }
+
+ if (splitbyte < rlen) {
+ inputStream.unread(buf, splitbyte, rlen - splitbyte);
+ }
+
+ parms = new HashMap<String, String>();
+ if(null == headers) {
+ headers = new HashMap<String, String>();
+ }
+
+ // Create a BufferedReader for parsing the header.
+ BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
+
+ // Decode the header into parms and header java properties
+ Map<String, String> pre = new HashMap<String, String>();
+ decodeHeader(hin, pre, parms, headers);
+
+ method = Method.lookup(pre.get("method"));
+ if (method == null) {
+ throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
+ }
+
+ uri = pre.get("uri");
+
+ cookies = new CookieHandler(headers);
+
+ // Ok, now do the serve()
+ Response r = serve(this);
+ if (r == null) {
+ throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
+ } else {
+ cookies.unloadQueue(r);
+ r.setRequestMethod(method);
+ r.send(outputStream);
+ }
+ } catch (SocketException e) {
+ // throw it out to close socket object (finalAccept)
+ throw e;
+ } catch (SocketTimeoutException ste) {
+ throw ste;
+ } catch (IOException ioe) {
+ Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+ r.send(outputStream);
+ safeClose(outputStream);
+ } catch (ResponseException re) {
+ Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
+ r.send(outputStream);
+ safeClose(outputStream);
+ } finally {
+ tempFileManager.clear();
+ }
+ }
+
+ @Override
+ public void parseBody(Map<String, String> files) throws IOException, ResponseException {
+ RandomAccessFile randomAccessFile = null;
+ BufferedReader in = null;
+ try {
+
+ randomAccessFile = getTmpBucket();
+
+ long size;
+ if (headers.containsKey("content-length")) {
+ size = Integer.parseInt(headers.get("content-length"));
+ } else if (splitbyte < rlen) {
+ size = rlen - splitbyte;
+ } else {
+ size = 0;
+ }
+
+ // Now read all the body and write it to f
+ byte[] buf = new byte[512];
+ while (rlen >= 0 && size > 0) {
+ rlen = inputStream.read(buf, 0, (int)Math.min(size, 512));
+ size -= rlen;
+ if (rlen > 0) {
+ randomAccessFile.write(buf, 0, rlen);
+ }
+ }
+
+ // Get the raw body as a byte []
+ ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
+ randomAccessFile.seek(0);
+
+ // Create a BufferedReader for easily reading it as string.
+ InputStream bin = new FileInputStream(randomAccessFile.getFD());
+ in = new BufferedReader(new InputStreamReader(bin));
+
+ // If the method is POST, there may be parameters
+ // in data section, too, read it:
+ if (Method.POST.equals(method)) {
+ String contentType = "";
+ String contentTypeHeader = headers.get("content-type");
+
+ StringTokenizer st = null;
+ if (contentTypeHeader != null) {
+ st = new StringTokenizer(contentTypeHeader, ",; ");
+ if (st.hasMoreTokens()) {
+ contentType = st.nextToken();
+ }
+ }
+
+ if ("multipart/form-data".equalsIgnoreCase(contentType)) {
+ // Handle multipart/form-data
+ if (!st.hasMoreTokens()) {
+ throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
+ }
+
+ String boundaryStartString = "boundary=";
+ int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
+ String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
+ if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
+ boundary = boundary.substring(1, boundary.length() - 1);
+ }
+
+ decodeMultipartData(boundary, fbuf, in, parms, files);
+ } else {
+ String postLine = "";
+ StringBuilder postLineBuffer = new StringBuilder();
+ char pbuf[] = new char[512];
+ int read = in.read(pbuf);
+ while (read >= 0 && !postLine.endsWith("\r\n")) {
+ postLine = String.valueOf(pbuf, 0, read);
+ postLineBuffer.append(postLine);
+ read = in.read(pbuf);
+ }
+ postLine = postLineBuffer.toString().trim();
+ // Handle application/x-www-form-urlencoded
+ if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
+ decodeParms(postLine, parms);
+ } else if (postLine.length() != 0) {
+ // Special case for raw POST data => create a special files entry "postData" with raw content data
+ files.put("postData", postLine);
+ }
+ }
+ } else if (Method.PUT.equals(method)) {
+ files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
+ }
+ } finally {
+ safeClose(randomAccessFile);
+ safeClose(in);
+ }
+ }
+
+ /**
+ * Decodes the sent headers and loads the data into Key/value pairs
+ */
+ private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)
+ throws ResponseException {
+ try {
+ // Read the request line
+ String inLine = in.readLine();
+ if (inLine == null) {
+ return;
+ }
+
+ StringTokenizer st = new StringTokenizer(inLine);
+ if (!st.hasMoreTokens()) {
+ throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
+ }
+
+ pre.put("method", st.nextToken());
+
+ if (!st.hasMoreTokens()) {
+ throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
+ }
+
+ String uri = st.nextToken();
+
+ // Decode parameters from the URI
+ int qmi = uri.indexOf('?');
+ if (qmi >= 0) {
+ decodeParms(uri.substring(qmi + 1), parms);
+ uri = decodePercent(uri.substring(0, qmi));
+ } else {
+ uri = decodePercent(uri);
+ }
+
+ // If there's another token, it's protocol version,
+ // followed by HTTP headers. Ignore version but parse headers.
+ // NOTE: this now forces header names lowercase since they are
+ // case insensitive and vary by client.
+ if (st.hasMoreTokens()) {
+ String line = in.readLine();
+ while (line != null && line.trim().length() > 0) {
+ int p = line.indexOf(':');
+ if (p >= 0)
+ headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
+ line = in.readLine();
+ }
+ }
+
+ pre.put("uri", uri);
+ } catch (IOException ioe) {
+ throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
+ }
+ }
+
+ /**
+ * Decodes the Multipart Body data and put it into Key/Value pairs.
+ */
+ private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms,
+ Map<String, String> files) throws ResponseException {
+ try {
+ int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
+ int boundarycount = 1;
+ String mpline = in.readLine();
+ while (mpline != null) {
+ if (!mpline.contains(boundary)) {
+ throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html");
+ }
+ boundarycount++;
+ Map<String, String> item = new HashMap<String, String>();
+ mpline = in.readLine();
+ while (mpline != null && mpline.trim().length() > 0) {
+ int p = mpline.indexOf(':');
+ if (p != -1) {
+ item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim());
+ }
+ mpline = in.readLine();
+ }
+ if (mpline != null) {
+ String contentDisposition = item.get("content-disposition");
+ if (contentDisposition == null) {
+ throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html");
+ }
+ StringTokenizer st = new StringTokenizer(contentDisposition, ";");
+ Map<String, String> disposition = new HashMap<String, String>();
+ while (st.hasMoreTokens()) {
+ String token = st.nextToken().trim();
+ int p = token.indexOf('=');
+ if (p != -1) {
+ disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim());
+ }
+ }
+ String pname = disposition.get("name");
+ pname = pname.substring(1, pname.length() - 1);
+
+ String value = "";
+ if (item.get("content-type") == null) {
+ while (mpline != null && !mpline.contains(boundary)) {
+ mpline = in.readLine();
+ if (mpline != null) {
+ int d = mpline.indexOf(boundary);
+ if (d == -1) {
+ value += mpline;
+ } else {
+ value += mpline.substring(0, d - 2);
+ }
+ }
+ }
+ } else {
+ if (boundarycount > bpositions.length) {
+ throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request");
+ }
+ int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
+ String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
+ files.put(pname, path);
+ value = disposition.get("filename");
+ value = value.substring(1, value.length() - 1);
+ do {
+ mpline = in.readLine();
+ } while (mpline != null && !mpline.contains(boundary));
+ }
+ parms.put(pname, value);
+ }
+ }
+ } catch (IOException ioe) {
+ throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
+ }
+ }
+
+ /**
+ * Find byte index separating header from body. It must be the last byte of the first two sequential new lines.
+ */
+ private int findHeaderEnd(final byte[] buf, int rlen) {
+ int splitbyte = 0;
+ while (splitbyte + 3 < rlen) {
+ if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
+ return splitbyte + 4;
+ }
+ splitbyte++;
+ }
+ return 0;
+ }
+
+ /**
+ * Find the byte positions where multipart boundaries start.
+ */
+ private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
+ int matchcount = 0;
+ int matchbyte = -1;
+ List<Integer> matchbytes = new ArrayList<Integer>();
+ for (int i = 0; i < b.limit(); i++) {
+ if (b.get(i) == boundary[matchcount]) {
+ if (matchcount == 0)
+ matchbyte = i;
+ matchcount++;
+ if (matchcount == boundary.length) {
+ matchbytes.add(matchbyte);
+ matchcount = 0;
+ matchbyte = -1;
+ }
+ } else {
+ i -= matchcount;
+ matchcount = 0;
+ matchbyte = -1;
+ }
+ }
+ int[] ret = new int[matchbytes.size()];
+ for (int i = 0; i < ret.length; i++) {
+ ret[i] = matchbytes.get(i);
+ }
+ return ret;
+ }
+
+ /**
+ * Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned.
+ */
+ private String saveTmpFile(ByteBuffer b, int offset, int len) {
+ String path = "";
+ if (len > 0) {
+ FileOutputStream fileOutputStream = null;
+ try {
+ TempFile tempFile = tempFileManager.createTempFile();
+ ByteBuffer src = b.duplicate();
+ fileOutputStream = new FileOutputStream(tempFile.getName());
+ FileChannel dest = fileOutputStream.getChannel();
+ src.position(offset).limit(offset + len);
+ dest.write(src.slice());
+ path = tempFile.getName();
+ } catch (Exception e) { // Catch exception if any
+ throw new Error(e); // we won't recover, so throw an error
+ } finally {
+ safeClose(fileOutputStream);
+ }
+ }
+ return path;
+ }
+
+ private RandomAccessFile getTmpBucket() {
+ try {
+ TempFile tempFile = tempFileManager.createTempFile();
+ return new RandomAccessFile(tempFile.getName(), "rw");
+ } catch (Exception e) {
+ throw new Error(e); // we won't recover, so throw an error
+ }
+ }
+
+ /**
+ * It returns the offset separating multipart file headers from the file's data.
+ */
+ private int stripMultipartHeaders(ByteBuffer b, int offset) {
+ int i;
+ for (i = offset; i < b.limit(); i++) {
+ if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') {
+ break;
+ }
+ }
+ return i + 1;
+ }
+
+ /**
+ * Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
+ * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map.
+ */
+ private void decodeParms(String parms, Map<String, String> p) {
+ if (parms == null) {
+ queryParameterString = "";
+ return;
+ }
+
+ queryParameterString = parms;
+ StringTokenizer st = new StringTokenizer(parms, "&");
+ while (st.hasMoreTokens()) {
+ String e = st.nextToken();
+ int sep = e.indexOf('=');
+ if (sep >= 0) {
+ p.put(decodePercent(e.substring(0, sep)).trim(),
+ decodePercent(e.substring(sep + 1)));
+ } else {
+ p.put(decodePercent(e).trim(), "");
+ }
+ }
+ }
+
+ @Override
+ public final Map<String, String> getParms() {
+ return parms;
+ }
+
+ public String getQueryParameterString() {
+ return queryParameterString;
+ }
+
+ @Override
+ public final Map<String, String> getHeaders() {
+ return headers;
+ }
+
+ @Override
+ public final String getUri() {
+ return uri;
+ }
+
+ @Override
+ public final Method getMethod() {
+ return method;
+ }
+
+ @Override
+ public final InputStream getInputStream() {
+ return inputStream;
+ }
+
+ @Override
+ public CookieHandler getCookies() {
+ return cookies;
+ }
+ }
+
+ public static class Cookie {
+ private String n, v, e;
+
+ public Cookie(String name, String value, String expires) {
+ n = name;
+ v = value;
+ e = expires;
+ }
+
+ public Cookie(String name, String value) {
+ this(name, value, 30);
+ }
+
+ public Cookie(String name, String value, int numDays) {
+ n = name;
+ v = value;
+ e = getHTTPTime(numDays);
+ }
+
+ public String getHTTPHeader() {
+ String fmt = "%s=%s; expires=%s";
+ return String.format(fmt, n, v, e);
+ }
+
+ public static String getHTTPTime(int days) {
+ Calendar calendar = Calendar.getInstance();
+ SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+ calendar.add(Calendar.DAY_OF_MONTH, days);
+ return dateFormat.format(calendar.getTime());
+ }
+ }
+
+ /**
+ * Provides rudimentary support for cookies.
+ * Doesn't support 'path', 'secure' nor 'httpOnly'.
+ * Feel free to improve it and/or add unsupported features.
+ *
+ * @author LordFokas
+ */
+ public class CookieHandler implements Iterable<String> {
+ private HashMap<String, String> cookies = new HashMap<String, String>();
+ private ArrayList<Cookie> queue = new ArrayList<Cookie>();
+
+ public CookieHandler(Map<String, String> httpHeaders) {
+ String raw = httpHeaders.get("cookie");
+ if (raw != null) {
+ String[] tokens = raw.split(";");
+ for (String token : tokens) {
+ String[] data = token.trim().split("=");
+ if (data.length == 2) {
+ cookies.put(data[0], data[1]);
+ }
+ }
+ }
+ }
+
+ @Override public Iterator<String> iterator() {
+ return cookies.keySet().iterator();
+ }
+
+ /**
+ * Read a cookie from the HTTP Headers.
+ *
+ * @param name The cookie's name.
+ * @return The cookie's value if it exists, null otherwise.
+ */
+ public String read(String name) {
+ return cookies.get(name);
+ }
+
+ /**
+ * Sets a cookie.
+ *
+ * @param name The cookie's name.
+ * @param value The cookie's value.
+ * @param expires How many days until the cookie expires.
+ */
+ public void set(String name, String value, int expires) {
+ queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
+ }
+
+ public void set(Cookie cookie) {
+ queue.add(cookie);
+ }
+
+ /**
+ * Set a cookie with an expiration date from a month ago, effectively deleting it on the client side.
+ *
+ * @param name The cookie name.
+ */
+ public void delete(String name) {
+ set(name, "-delete-", -30);
+ }
+
+ /**
+ * Internally used by the webserver to add all queued cookies into the Response's HTTP Headers.
+ *
+ * @param response The Response object to which headers the queued cookies will be added.
+ */
+ public void unloadQueue(Response response) {
+ for (Cookie cookie : queue) {
+ response.addHeader("Set-Cookie", cookie.getHTTPHeader());
+ }
+ }
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java
new file mode 100644
index 000000000..4e5d0297f
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java
@@ -0,0 +1,109 @@
+package de.test.antennapod.util.syndication;
+
+import android.test.InstrumentationTestCase;
+import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.Map;
+
+/**
+ * Test class for FeedDiscoverer
+ */
+public class FeedDiscovererTest extends InstrumentationTestCase {
+
+ private FeedDiscoverer fd;
+
+ private File testDir;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ fd = new FeedDiscoverer();
+ testDir = getInstrumentation().getTargetContext().getExternalFilesDir("FeedDiscovererTest");
+ testDir.mkdir();
+ assertTrue(testDir.exists());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ FileUtils.deleteDirectory(testDir);
+ super.tearDown();
+ }
+
+ private String createTestHtmlString(String rel, String type, String href, String title) {
+ return String.format("<html><head><title>Test</title><link rel=\"%s\" type=\"%s\" href=\"%s\" title=\"%s\"></head><body></body></html>",
+ rel, type, href, title);
+ }
+
+ private String createTestHtmlString(String rel, String type, String href) {
+ return String.format("<html><head><title>Test</title><link rel=\"%s\" type=\"%s\" href=\"%s\"></head><body></body></html>",
+ rel, type, href);
+ }
+
+ private void checkFindUrls(boolean isAlternate, boolean isRss, boolean withTitle, boolean isAbsolute, boolean fromString) throws Exception {
+ final String title = "Test title";
+ final String hrefAbs = "http://example.com/feed";
+ final String hrefRel = "/feed";
+ final String base = "http://example.com";
+
+ final String rel = (isAlternate) ? "alternate" : "feed";
+ final String type = (isRss) ? "application/rss+xml" : "application/atom+xml";
+ final String href = (isAbsolute) ? hrefAbs : hrefRel;
+
+ Map<String, String> res;
+ String html = (withTitle) ? createTestHtmlString(rel, type, href, title)
+ : createTestHtmlString(rel, type, href);
+ if (fromString) {
+ res = fd.findLinks(html, base);
+ } else {
+ File testFile = new File(testDir, "feed");
+ FileOutputStream out = new FileOutputStream(testFile);
+ IOUtils.write(html, out);
+ out.close();
+ res = fd.findLinks(testFile, base);
+ }
+
+ assertNotNull(res);
+ assertEquals(1, res.size());
+ for (String key : res.keySet()) {
+ assertEquals(hrefAbs, key);
+ }
+ assertTrue(res.containsKey(hrefAbs));
+ if (withTitle) {
+ assertEquals(title, res.get(hrefAbs));
+ } else {
+ assertEquals(href, res.get(hrefAbs));
+ }
+ }
+
+ public void testAlternateRSSWithTitleAbsolute() throws Exception {
+ checkFindUrls(true, true, true, true, true);
+ }
+
+ public void testAlternateRSSWithTitleRelative() throws Exception {
+ checkFindUrls(true, true, true, false, true);
+ }
+
+ public void testAlternateRSSNoTitleAbsolute() throws Exception {
+ checkFindUrls(true, true, false, true, true);
+ }
+
+ public void testAlternateRSSNoTitleRelative() throws Exception {
+ checkFindUrls(true, true, false, false, true);
+ }
+
+ public void testAlternateAtomWithTitleAbsolute() throws Exception {
+ checkFindUrls(true, false, true, true, true);
+ }
+
+ public void testFeedAtomWithTitleAbsolute() throws Exception {
+ checkFindUrls(false, false, true, true, true);
+ }
+
+ public void testAlternateRSSWithTitleAbsoluteFromFile() throws Exception {
+ checkFindUrls(true, true, true, true, false);
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java
new file mode 100644
index 000000000..69cc827ec
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java
@@ -0,0 +1,118 @@
+package de.test.antennapod.util.syndication.feedgenerator;
+
+import android.util.Xml;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Creates Atom feeds. See FeedGenerator for more information.
+ */
+public class AtomGenerator implements FeedGenerator{
+
+ private static final String NS_ATOM = "http://www.w3.org/2005/Atom";
+
+ public static final long FEATURE_USE_RFC3339LOCAL = 1;
+
+ @Override
+ public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException {
+ if (feed == null) throw new IllegalArgumentException("feed = null");
+ if (outputStream == null) throw new IllegalArgumentException("outputStream = null");
+ if (encoding == null) throw new IllegalArgumentException("encoding = null");
+
+ XmlSerializer xml = Xml.newSerializer();
+ xml.setOutput(outputStream, encoding);
+ xml.startDocument(encoding, null);
+
+ xml.startTag(null, "feed");
+ xml.attribute(null, "xmlns", NS_ATOM);
+
+ // Write Feed data
+ if (feed.getIdentifyingValue() != null) {
+ xml.startTag(null, "id");
+ xml.text(feed.getIdentifyingValue());
+ xml.endTag(null, "id");
+ }
+ if (feed.getTitle() != null) {
+ xml.startTag(null, "title");
+ xml.text(feed.getTitle());
+ xml.endTag(null, "title");
+ }
+ if (feed.getLink() != null) {
+ xml.startTag(null, "link");
+ xml.attribute(null, "rel", "alternate");
+ xml.attribute(null, "href", feed.getLink());
+ xml.endTag(null, "link");
+ }
+ if (feed.getDescription() != null) {
+ xml.startTag(null, "subtitle");
+ xml.text(feed.getDescription());
+ xml.endTag(null, "subtitle");
+ }
+
+ if (feed.getPaymentLink() != null) {
+ GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), false);
+ }
+
+ // Write FeedItem data
+ if (feed.getItems() != null) {
+ for (FeedItem item : feed.getItems()) {
+ xml.startTag(null, "entry");
+
+ if (item.getIdentifyingValue() != null) {
+ xml.startTag(null, "id");
+ xml.text(item.getIdentifyingValue());
+ xml.endTag(null, "id");
+ }
+ if (item.getTitle() != null) {
+ xml.startTag(null, "title");
+ xml.text(item.getTitle());
+ xml.endTag(null, "title");
+ }
+ if (item.getLink() != null) {
+ xml.startTag(null, "link");
+ xml.attribute(null, "rel", "alternate");
+ xml.attribute(null, "href", item.getLink());
+ xml.endTag(null, "link");
+ }
+ if (item.getPubDate() != null) {
+ xml.startTag(null, "published");
+ if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) {
+ xml.text(SyndDateUtils.formatRFC3339Local(item.getPubDate()));
+ } else {
+ xml.text(SyndDateUtils.formatRFC3339UTC(item.getPubDate()));
+ }
+ xml.endTag(null, "published");
+ }
+ if (item.getDescription() != null) {
+ xml.startTag(null, "content");
+ xml.text(item.getDescription());
+ xml.endTag(null, "content");
+ }
+ if (item.getMedia() != null) {
+ FeedMedia media = item.getMedia();
+ xml.startTag(null, "link");
+ xml.attribute(null, "rel", "enclosure");
+ xml.attribute(null, "href", media.getDownload_url());
+ xml.attribute(null, "type", media.getMime_type());
+ xml.attribute(null, "length", String.valueOf(media.getSize()));
+ xml.endTag(null, "link");
+ }
+
+ if (item.getPaymentLink() != null) {
+ GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), false);
+ }
+
+ xml.endTag(null, "entry");
+ }
+ }
+
+ xml.endTag(null, "feed");
+ xml.endDocument();
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java
new file mode 100644
index 000000000..fe5afd847
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java
@@ -0,0 +1,28 @@
+package de.test.antennapod.util.syndication.feedgenerator;
+
+import de.danoeh.antennapod.core.feed.Feed;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Generates a machine-readable, platform-independent representation of a Feed object.
+ */
+public interface FeedGenerator {
+
+ /**
+ * Creates a machine-readable, platform-independent representation of a given
+ * Feed object and writes it to the given OutputStream.
+ * <p/>
+ * The representation might not be compliant with its specification if the feed
+ * is missing certain attribute values. This is intentional because the FeedGenerator is
+ * used for creating test data.
+ *
+ * @param feed The feed that should be written. Must not be null.
+ * @param outputStream The output target that the feed will be written to. The outputStream is not closed after
+ * the method's execution Must not be null.
+ * @param encoding The encoding to use. Must not be null.
+ * @param flags Optional argument for enabling implementation-dependent features.
+ */
+ public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException;
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java
new file mode 100644
index 000000000..e7cbb1b42
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java
@@ -0,0 +1,21 @@
+package de.test.antennapod.util.syndication.feedgenerator;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * Utility methods for FeedGenerator
+ */
+public class GeneratorUtil {
+
+ public static void addPaymentLink(XmlSerializer xml, String paymentLink, boolean withNamespace) throws IOException {
+ String ns = (withNamespace) ? "http://www.w3.org/2005/Atom" : null;
+ xml.startTag(ns, "link");
+ xml.attribute(null, "rel", "payment");
+ xml.attribute(null, "title", "Flattr this!");
+ xml.attribute(null, "href", paymentLink);
+ xml.attribute(null, "type", "text/html");
+ xml.endTag(ns, "link");
+ }
+}
diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java
new file mode 100644
index 000000000..d37434f06
--- /dev/null
+++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java
@@ -0,0 +1,110 @@
+package de.test.antennapod.util.syndication.feedgenerator;
+
+import android.util.Xml;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Creates RSS 2.0 feeds. See FeedGenerator for more information.
+ */
+public class RSS2Generator implements FeedGenerator{
+
+ public static final long FEATURE_WRITE_GUID = 1;
+
+ @Override
+ public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException {
+ if (feed == null) throw new IllegalArgumentException("feed = null");
+ if (outputStream == null) throw new IllegalArgumentException("outputStream = null");
+ if (encoding == null) throw new IllegalArgumentException("encoding = null");
+
+ XmlSerializer xml = Xml.newSerializer();
+ xml.setOutput(outputStream, encoding);
+ xml.startDocument(encoding, null);
+
+ xml.setPrefix("atom", "http://www.w3.org/2005/Atom");
+ xml.startTag(null, "rss");
+ xml.attribute(null, "version", "2.0");
+ xml.startTag(null, "channel");
+
+ // Write Feed data
+ if (feed.getTitle() != null) {
+ xml.startTag(null, "title");
+ xml.text(feed.getTitle());
+ xml.endTag(null, "title");
+ }
+ if (feed.getDescription() != null) {
+ xml.startTag(null, "description");
+ xml.text(feed.getDescription());
+ xml.endTag(null, "description");
+ }
+ if (feed.getLink() != null) {
+ xml.startTag(null, "link");
+ xml.text(feed.getLink());
+ xml.endTag(null, "link");
+ }
+ if (feed.getLanguage() != null) {
+ xml.startTag(null, "language");
+ xml.text(feed.getLanguage());
+ xml.endTag(null, "language");
+ }
+
+ if (feed.getPaymentLink() != null) {
+ GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), true);
+ }
+
+ // Write FeedItem data
+ if (feed.getItems() != null) {
+ for (FeedItem item : feed.getItems()) {
+ xml.startTag(null, "item");
+
+ if (item.getTitle() != null) {
+ xml.startTag(null, "title");
+ xml.text(item.getTitle());
+ xml.endTag(null, "title");
+ }
+ if (item.getDescription() != null) {
+ xml.startTag(null, "description");
+ xml.text(item.getDescription());
+ xml.endTag(null, "description");
+ }
+ if (item.getLink() != null) {
+ xml.startTag(null, "link");
+ xml.text(item.getLink());
+ xml.endTag(null, "link");
+ }
+ if (item.getPubDate() != null) {
+ xml.startTag(null, "pubDate");
+ xml.text(SyndDateUtils.formatRFC822Date(item.getPubDate()));
+ xml.endTag(null, "pubDate");
+ }
+ if ((flags & FEATURE_WRITE_GUID) != 0) {
+ xml.startTag(null, "guid");
+ xml.text(item.getItemIdentifier());
+ xml.endTag(null, "guid");
+ }
+ if (item.getMedia() != null) {
+ xml.startTag(null, "enclosure");
+ xml.attribute(null, "url", item.getMedia().getDownload_url());
+ xml.attribute(null, "length", String.valueOf(item.getMedia().getSize()));
+ xml.attribute(null, "type", item.getMedia().getMime_type());
+ xml.endTag(null, "enclosure");
+ }
+ if (item.getPaymentLink() != null) {
+ GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), true);
+ }
+
+ xml.endTag(null, "item");
+ }
+ }
+
+ xml.endTag(null, "channel");
+ xml.endTag(null, "rss");
+
+ xml.endDocument();
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..cce3d2dc0
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,317 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="de.danoeh.antennapod"
+ android:versionCode="41"
+ android:versionName="0.9.9.4">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+
+ <supports-screens
+ android:anyDensity="true"
+ android:largeScreens="true"
+ android:normalScreens="true"
+ android:smallScreens="true"
+ android:xlargeScreens="true"/>
+
+ <uses-feature
+ android:name="android.hardware.screen.portrait"
+ android:required="false"/>
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false"/>
+
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+
+ <application
+ android:name="de.danoeh.antennapod.PodcastApp"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:backupAgent=".core.backup.OpmlBackupAgent"
+ android:restoreAnyVersion="true"
+ android:logo="@drawable/ic_launcher"
+ android:theme="@style/Theme.AntennaPod.Light">
+ <meta-data
+ android:name="com.google.android.backup.api_key"
+ android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
+
+ <activity
+ android:name=".activity.MainActivity"
+ android:configChanges="keyboardHidden|orientation"
+ android:launchMode="singleTask"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".activity.AudioplayerActivity"
+ android:launchMode="singleTop">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.MainActivity"/>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="file"/>
+ <data android:mimeType="audio/*"/>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".activity.DownloadAuthenticationActivity"
+ android:launchMode="singleInstance"/>
+
+ <activity
+ android:name=".activity.PreferenceActivity"
+ android:configChanges="keyboardHidden|orientation"
+ android:label="@string/settings_label">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.MainActivity"/>
+ </activity>
+
+ <activity android:name=".activity.FeedInfoActivity">
+ </activity>
+
+ <service
+ android:name=".service.PlayerWidgetService"
+ android:enabled="true"
+ android:exported="false">
+ </service>
+
+ <receiver android:name=".receiver.PlayerWidget">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE"/>
+ </intent-filter>
+
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/player_widget_info"/>
+
+ <intent-filter>
+ <action android:name="de.danoeh.antennapod.STOP_WIDGET_UPDATE"/>
+ </intent-filter>
+ </receiver>
+
+ <activity android:name=".activity.StorageErrorActivity">
+ </activity>
+ <activity
+ android:name=".activity.FlattrAuthActivity"
+ android:label="@string/flattr_auth_label">
+ <intent-filter>
+ <action android:name=".activities.FlattrAuthActivity"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data
+ android:host="de.danoeh.antennapod"
+ android:scheme="flattr4j"/>
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".activity.AboutActivity"
+ android:label="@string/about_pref">
+ </activity>
+ <activity
+ android:name=".activity.OpmlImportFromPathActivity"
+ android:configChanges="keyboardHidden|orientation"
+ android:label="@string/opml_import_label">
+ </activity>
+ <activity
+ android:name=".activity.OpmlImportFromIntentActivity"
+ android:configChanges="keyboardHidden|orientation"
+ android:label="@string/opml_import_label">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data
+ android:host="*"
+ android:mimeType="*/*"
+ android:pathPattern=".*\\.opml"
+ android:scheme="file"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data
+ android:host="*"
+ android:pathPattern=".*\\.opml"
+ android:scheme="file"
+ android:mimeType="text/x-opml"/>
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".activity.OpmlFeedChooserActivity"
+ android:label="@string/opml_import_label">
+ </activity>
+
+ <activity
+ android:name=".activity.VideoplayerActivity"
+ android:configChanges="keyboardHidden|orientation"
+ android:screenOrientation="landscape">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.MainActivity"/>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="file"/>
+ <data android:mimeType="video/*"/>
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".activity.DirectoryChooserActivity"
+ android:label="@string/choose_data_directory">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.PreferenceActivity"/>
+ </activity>
+
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.PreferenceActivity"/>
+
+ <activity
+ android:name=".activity.DefaultOnlineFeedViewActivity"
+ android:configChanges="orientation">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.MainActivity"/>
+
+ <!-- URLs ending with '.xml' or '.rss' -->
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="http"/>
+ <data android:scheme="https"/>
+ <data android:host="*"/>
+ <data android:pathPattern=".*\\.xml"/>
+ <data android:pathPattern=".*\\.rss"/>
+ </intent-filter>
+
+ <!-- Feedburner URLs -->
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="http"/>
+ <data android:scheme="https"/>
+ <data android:host="feeds.feedburner.com"/>
+ <data android:host="feedproxy.google.com"/>
+ <data android:host="feeds2.feedburner.com"/>
+ <data android:host="feedsproxy.google.com"/>
+ </intent-filter>
+
+ <!-- Files with mimeType rss/xml/atom -->
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="http"/>
+ <data android:scheme="https"/>
+ <data android:mimeType="text/xml"/>
+ <data android:mimeType="application/rss+xml"/>
+ <data android:mimeType="application/atom+xml"/>
+ <data android:mimeType="application/xml"/>
+ </intent-filter>
+
+ <!-- Podcast protocols -->
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="itpc"/>
+ <data android:scheme="pcast"/>
+ <data android:scheme="feed"/>
+ <data android:scheme="antennapod-subscribe"/>
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.intent.action.SEND"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+
+ <data android:mimeType="text/plain"/>
+ </intent-filter>
+
+ </activity>
+
+ <activity
+ android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"
+ android:configChanges="orientation"
+ android:label="@string/gpodnet_auth_label"
+ android:screenOrientation="portrait">
+ <intent-filter>
+ <action android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.PreferenceActivity"/>
+ </activity>
+
+ <receiver android:name=".receiver.ConnectivityActionReceiver">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name=".receiver.SPAReceiver">
+ <intent-filter>
+ <action android:name="de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"/>
+ </intent-filter>
+ </receiver>
+ <receiver android:name="de.danoeh.antennapod.core.receiver.AlarmUpdateReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_REPLACED"/>
+
+ <data
+ android:path="de.danoeh.antennapod"
+ android:scheme="package"/>
+ </intent-filter>
+ </receiver>
+ </application>
+
+</manifest>
diff --git a/app/src/main/assets/LICENSE.html b/app/src/main/assets/LICENSE.html
new file mode 100644
index 000000000..d38547791
--- /dev/null
+++ b/app/src/main/assets/LICENSE.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
+ <title>MIT License</title>
+ </head>
+ <body>
+<p>Copyright (c) 2012 Daniel Oeh</p>
+
+<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
+
+<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
+
+</p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
+
+ </body>
+</html> \ No newline at end of file
diff --git a/app/src/main/assets/LICENSE_APACHE_COMMONS.txt b/app/src/main/assets/LICENSE_APACHE_COMMONS.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_APACHE_COMMONS.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_BETTERPICKERS.txt b/app/src/main/assets/LICENSE_BETTERPICKERS.txt
new file mode 100644
index 000000000..80830ed73
--- /dev/null
+++ b/app/src/main/assets/LICENSE_BETTERPICKERS.txt
@@ -0,0 +1,13 @@
+Copyright 2013 Derek Brameyer
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_DSLV.txt b/app/src/main/assets/LICENSE_DSLV.txt
new file mode 100644
index 000000000..2a2de04a3
--- /dev/null
+++ b/app/src/main/assets/LICENSE_DSLV.txt
@@ -0,0 +1,16 @@
+A subclass of the Android ListView component that enables drag
+and drop re-ordering of list items.
+
+Copyright 2012 Carl Bauer
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_FLATTR4J.txt b/app/src/main/assets/LICENSE_FLATTR4J.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_FLATTR4J.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_JSOUP.txt b/app/src/main/assets/LICENSE_JSOUP.txt
new file mode 100644
index 000000000..f3ef71dbf
--- /dev/null
+++ b/app/src/main/assets/LICENSE_JSOUP.txt
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2009, 2010, 2011, 2012, 2013 Jonathan Hedley <jonathan@hedley.net>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt b/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_OKHTTP.txt b/app/src/main/assets/LICENSE_OKHTTP.txt
new file mode 100644
index 000000000..90edcee40
--- /dev/null
+++ b/app/src/main/assets/LICENSE_OKHTTP.txt
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_OKIO.txt b/app/src/main/assets/LICENSE_OKIO.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_OKIO.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_PICASSO.txt b/app/src/main/assets/LICENSE_PICASSO.txt
new file mode 100644
index 000000000..0bf6b9f8e
--- /dev/null
+++ b/app/src/main/assets/LICENSE_PICASSO.txt
@@ -0,0 +1,13 @@
+Copyright 2013 Square, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_PRESTO.txt b/app/src/main/assets/LICENSE_PRESTO.txt
new file mode 100644
index 000000000..b4b1a8cf5
--- /dev/null
+++ b/app/src/main/assets/LICENSE_PRESTO.txt
@@ -0,0 +1,13 @@
+Copyright 2011, Aocate, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/Roboto-Light.ttf b/app/src/main/assets/Roboto-Light.ttf
new file mode 100644
index 000000000..13bf13af0
--- /dev/null
+++ b/app/src/main/assets/Roboto-Light.ttf
Binary files differ
diff --git a/app/src/main/assets/Roboto.ttf b/app/src/main/assets/Roboto.ttf
new file mode 100644
index 000000000..0ba95c98c
--- /dev/null
+++ b/app/src/main/assets/Roboto.ttf
Binary files differ
diff --git a/app/src/main/assets/about.html b/app/src/main/assets/about.html
new file mode 100644
index 000000000..115a0a5c9
--- /dev/null
+++ b/app/src/main/assets/about.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
+ <style type="text/css">
+
+ * {
+ font-family: Helvetica
+ }
+ header {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ padding-bottom: 500px;
+
+
+ }
+
+ versiontag {
+ color: gray;
+ }
+
+ h1 {
+ font-size: 15pt;
+ }
+
+ h2 {
+ font-size: 13pt;
+ }
+
+ a {
+ font-size: 14px;
+ color: #00A8DF;
+ text-decoration: none;
+ }
+
+ </style>
+ <title>About AntennaPod</title>
+</head>
+<body>
+<div id="header" align="center">
+ <img src="logo.png" alt="Logo" width="100px" height="100px"/>
+
+ <p>AntennaPod, Version 0.9.9.4</p>
+
+ <p>Copyright © 2014 Daniel Oeh</p>
+
+ <p>Licensed under the MIT License <a href="LICENSE.html">(View)</a></p>
+</div>
+<h1>Used libraries</h1>
+
+<h2>NineOldAndroids <a href="http://nineoldandroids.com">(Link)</a></h2>
+by Jake Wharton, licensed under the Apache 2.0 license <a href="LICENSE_NINE_OLD_ANDROIDS.txt">(View)</a>
+
+<h2>Apache Commons <a href="http://commons.apache.org/">(Link)</a></h2>
+by The Apache Software Foundation, licensed under the Apache 2.0 license <a
+ href="LICENSE_APACHE_COMMONS.txt">(View)</a>
+
+<h2>flattr4j <a href="http://www.shredzone.org/projects/flattr4j/wiki">(Link)</a></h2>
+licensed under the Apache 2.0 license <a href="LICENSE_FLATTR4J.txt">(View)</a>
+
+<h2>drag-sort-listview <a href="https://github.com/bauerca/drag-sort-listview">(Link)</a></h2>
+licensed under the Apache 2.0 license <a href="LICENSE_DSLV.txt">(View)</a>
+
+<h2>Presto Client <a href="http://www.aocate.com/presto/">(Link)</a></h2>
+licensed under the Apache 2.0 license <a href="LICENSE_PRESTO.txt">(View)</a>
+
+<h2>Better Pickers <a href="https://github.com/derekbrameyer/android-betterpickers">(Link)</a></h2>
+licensed under the Apache 2.0 license <a href="LICENSE_BETTERPICKERS.txt">(View)</a>
+
+<h2>jsoup <a href="http://jsoup.org/">(Link)</a></h2>
+licensed under the MIT license <a href="LICENSE_JSOUP.txt">(View)</a>
+</body>
+<h2>Picasso <a href="https://github.com/square/picasso">(Link)</a></h2>
+licensed under the Apache 2.0 license <a href="LICENSE_PICASSO.txt">(View)</a>
+
+<h2>OkHttp <a href="https://github.com/square/okhttp">(Link)</a></h2>
+licensed under the Apache 2.0 license <a href="LICENSE_OKHTTP.txt">(View)</a>
+
+<h2>Okio <a href="https://github.com/square/okio">(Link)</a></h2>
+licensed under the Apache 2.0 license <a href="LICENSE_OKIO.txt">(View)</a>
+
+</html>
diff --git a/app/src/main/assets/logo.png b/app/src/main/assets/logo.png
new file mode 100755
index 000000000..d0e988a6d
--- /dev/null
+++ b/app/src/main/assets/logo.png
Binary files differ
diff --git a/app/src/main/assets/testfile.mp3 b/app/src/main/assets/testfile.mp3
new file mode 100644
index 000000000..f15faadf3
--- /dev/null
+++ b/app/src/main/assets/testfile.mp3
Binary files differ
diff --git a/app/src/main/java/de/danoeh/antennapod/AppConfig.java b/app/src/main/java/de/danoeh/antennapod/AppConfig.java
new file mode 100644
index 000000000..24f13d4a3
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/AppConfig.java
@@ -0,0 +1,7 @@
+package de.danoeh.antennapod;
+
+public final class AppConfig {
+ /** Should be used when setting User-Agent header for HTTP-requests. */
+ public final static String USER_AGENT = "AntennaPod/0.9.9.4";
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java
new file mode 100644
index 000000000..87474dbdd
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod;
+
+import android.app.Application;
+import android.content.res.Configuration;
+
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.spa.SPAUtil;
+
+/** Main application class. */
+public class PodcastApp extends Application {
+
+ // make sure that ClientConfigurator executes its static code
+ static {
+ try {
+ Class.forName("de.danoeh.antennapod.config.ClientConfigurator");
+ } catch (Exception e) {
+ throw new RuntimeException("ClientConfigurator not found");
+ }
+ }
+
+ private static final String TAG = "PodcastApp";
+
+ private static float LOGICAL_DENSITY;
+
+ private static PodcastApp singleton;
+
+ public static PodcastApp getInstance() {
+ return singleton;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ singleton = this;
+ LOGICAL_DENSITY = getResources().getDisplayMetrics().density;
+
+ UserPreferences.createInstance(this);
+ PlaybackPreferences.createInstance(this);
+ EventDistributor.getInstance();
+
+ SPAUtil.sendSPAppsQueryFeedsIntent(this);
+ }
+
+ public static float getLogicalDensity() {
+ return LOGICAL_DENSITY;
+ }
+
+ public boolean isLargeScreen() {
+ return (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE
+ || (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE;
+
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java
new file mode 100644
index 000000000..cf7de1709
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.activity;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import de.danoeh.antennapod.R;
+
+/** Displays the 'about' screen */
+public class AboutActivity extends ActionBarActivity {
+
+ private WebView webview;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().hide();
+ setContentView(R.layout.about);
+ webview = (WebView) findViewById(R.id.webvAbout);
+ webview.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ view.loadUrl(url);
+ return false;
+ }
+
+ });
+ webview.loadUrl("file:///android_asset/about.html");
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java
new file mode 100644
index 000000000..f9001adad
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java
@@ -0,0 +1,746 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.ListFragment;
+import android.support.v4.widget.DrawerLayout;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView.ScaleType;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.StringUtils;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.ChapterListAdapter;
+import de.danoeh.antennapod.adapter.NavListAdapter;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.feed.SimpleChapter;
+import de.danoeh.antennapod.fragment.CoverFragment;
+import de.danoeh.antennapod.fragment.ItemDescriptionFragment;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+import de.danoeh.antennapod.core.util.playback.ExternalMedia;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlaybackController;
+
+/**
+ * Activity for playing audio files.
+ */
+public class AudioplayerActivity extends MediaplayerActivity implements ItemDescriptionFragment.ItemDescriptionFragmentCallback,
+ NavDrawerActivity {
+ 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 DrawerLayout drawerLayout;
+ private NavListAdapter navAdapter;
+ private ListView navList;
+ private ActionBarDrawerToggle drawerToggle;
+
+ 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 Button butPlaybackSpeed;
+ private ImageButton butNavLeft;
+ private ImageButton butNavRight;
+
+ private void resetFragmentView() {
+ FragmentTransaction fT = getSupportFragmentManager().beginTransaction();
+
+ if (coverFragment != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Removing cover fragment");
+ fT.remove(coverFragment);
+ }
+ if (descriptionFragment != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Removing description fragment");
+ fT.remove(descriptionFragment);
+ }
+ if (chapterFragment != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Removing chapter fragment");
+ fT.remove(chapterFragment);
+ }
+ if (currentlyShownFragment != null) {
+ if (BuildConfig.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 (BuildConfig.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 (BuildConfig.DEBUG)
+ Log.d(TAG, "onStop");
+ cancelLoadTask();
+ EventDistributor.getInstance().unregister(contentUpdate);
+
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+ detachedFragments = new Fragment[NUM_CONTENT_FRAGMENTS];
+ }
+
+ private void savePreferences() {
+ if (BuildConfig.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);
+ drawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ // super.onSaveInstanceState(outState); would cause crash
+ if (BuildConfig.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 (BuildConfig.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 (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Couldn't restore from preferences: controller or media was null");
+ } else {
+ if (BuildConfig.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 (StringUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
+ Intent intent = getIntent();
+ if (BuildConfig.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);
+ }
+
+ EventDistributor.getInstance().register(contentUpdate);
+ loadData();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
+
+ @Override
+ protected void onAwaitingVideoSurface() {
+ if (BuildConfig.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 (BuildConfig.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, 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 (BuildConfig.DEBUG)
+ Log.d(TAG, "Reattaching fragment at position "
+ + pos);
+ ft.attach(detachedFragments[pos]);
+ } else {
+ ft.add(R.id.contentView, currentlyShownFragment);
+ }
+ 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) {
+
+ butNavRight.setTag(R.id.imageloader_key, null);
+ butNavLeft.setTag(R.id.imageloader_key, 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() {
+ PicassoProvider.getMediaMetadataPicassoInstance(AudioplayerActivity.this)
+ .load(media.getImageUri())
+ .fit()
+ .into(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() {
+ PicassoProvider.getMediaMetadataPicassoInstance(AudioplayerActivity.this)
+ .load(media.getImageUri())
+ .fit()
+ .into(butNavLeft);
+ }
+
+ });
+ butNavLeft.setContentDescription(getString(buttonTexts[2]));
+
+ butNavRight.setImageDrawable(drawables.getDrawable(0));
+ butNavRight.setContentDescription(getString(buttonTexts[0]));
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void setupGUI() {
+ super.setupGUI();
+ resetFragmentView();
+ drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ navList = (ListView) findViewById(R.id.nav_list);
+ txtvTitle = (TextView) findViewById(R.id.txtvTitle);
+ butNavLeft = (ImageButton) findViewById(R.id.butNavLeft);
+ butNavRight = (ImageButton) findViewById(R.id.butNavRight);
+ butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed);
+
+ TypedArray typedArray = obtainStyledAttributes(new int[]{R.attr.nav_drawer_toggle});
+ drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, typedArray.getResourceId(0, 0), R.string.drawer_open, R.string.drawer_close) {
+ String currentTitle = getSupportActionBar().getTitle().toString();
+
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ currentTitle = getSupportActionBar().getTitle().toString();
+ getSupportActionBar().setTitle(R.string.app_name);
+ supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ super.onDrawerClosed(drawerView);
+ getSupportActionBar().setTitle(currentTitle);
+ supportInvalidateOptionsMenu();
+ }
+ };
+ typedArray.recycle();
+ drawerToggle.setDrawerIndicatorEnabled(false);
+ drawerLayout.setDrawerListener(drawerToggle);
+
+ navAdapter = new NavListAdapter(itemAccess, this);
+ navList.setAdapter(navAdapter);
+ navList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ int viewType = parent.getAdapter().getItemViewType(position);
+ if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) {
+ int relPos = (viewType == NavListAdapter.VIEW_TYPE_NAV) ? position : position - NavListAdapter.SUBSCRIPTION_OFFSET;
+ Intent intent = new Intent(AudioplayerActivity.this, MainActivity.class);
+ intent.putExtra(MainActivity.EXTRA_NAV_TYPE, viewType);
+ intent.putExtra(MainActivity.EXTRA_NAV_INDEX, relPos);
+ startActivity(intent);
+ }
+ drawerLayout.closeDrawer(navList);
+ }
+ });
+ drawerToggle.syncState();
+
+ 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];
+ } else {
+ newSpeed = "1.0";
+ }
+
+ for (int i = 0; i < availableSpeeds.length; i++) {
+ if (availableSpeeds[i].equals(currentSpeed)) {
+ if (i == availableSpeeds.length - 1) {
+ newSpeed = availableSpeeds[0];
+ } else {
+ newSpeed = availableSpeeds[i + 1];
+ }
+ break;
+ }
+ }
+ UserPreferences.setPlaybackSpeed(newSpeed);
+ controller.setPlaybackSpeed(Float.parseFloat(newSpeed));
+ }
+ }
+ });
+
+ butPlaybackSpeed.setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ VariableSpeedDialog.showDialog(AudioplayerActivity.this);
+ return true;
+ }
+ });
+ }
+
+ @Override
+ protected void onPlaybackSpeedChange() {
+ super.onPlaybackSpeedChange();
+ updateButPlaybackSpeed();
+ }
+
+ private void updateButPlaybackSpeed() {
+ if (controller != null && controller.canSetPlaybackSpeed()) {
+ 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());
+ if (media.getChapters() != null) {
+ butNavRight.setVisibility(View.VISIBLE);
+ } else {
+ butNavRight.setVisibility(View.INVISIBLE);
+ }
+
+
+ if (currentlyShownPosition == -1) {
+ if (!restoreFromPreferences()) {
+ switchToFragment(POS_COVER);
+ }
+ }
+ if (currentlyShownFragment instanceof AudioplayerContentFragment) {
+ ((AudioplayerContentFragment) currentlyShownFragment)
+ .onDataSetChanged(media);
+ }
+
+ if (controller == null
+ || !controller.canSetPlaybackSpeed()) {
+ butPlaybackSpeed.setVisibility(View.GONE);
+ } else {
+ butPlaybackSpeed.setVisibility(View.VISIBLE);
+ }
+
+ 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 (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "ReloadNotification received, switching to Videoplayer now");
+ finish();
+ startActivity(new Intent(this, VideoplayerActivity.class));
+
+ }
+ }
+
+ @Override
+ protected void onBufferStart() {
+ postStatusMsg(R.string.player_buffering_msg);
+ }
+
+ @Override
+ protected void onBufferEnd() {
+ clearStatusMsg();
+ }
+
+ @Override
+ public PlaybackController getPlaybackController() {
+ return controller;
+ }
+
+ @Override
+ public boolean isDrawerOpen() {
+ return drawerLayout != null && navList != null && drawerLayout.isDrawerOpen(navList);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (!MenuItemUtils.isActivityDrawerOpen(this)) {
+ return super.onCreateOptionsMenu(menu);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ if (!MenuItemUtils.isActivityDrawerOpen(this)) {
+ return super.onPrepareOptionsMenu(menu);
+ } else {
+ return false;
+ }
+ }
+
+ public interface AudioplayerContentFragment {
+ public void onDataSetChanged(Playable media);
+ }
+
+ @Override
+ protected int getContentViewResourceId() {
+ return R.layout.audioplayer_activity;
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (drawerToggle != null && drawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private DBReader.NavDrawerData navDrawerData;
+ private AsyncTask<Void, Void, DBReader.NavDrawerData> loadTask;
+
+ private void loadData() {
+ loadTask = new AsyncTask<Void, Void, DBReader.NavDrawerData>() {
+ @Override
+ protected DBReader.NavDrawerData doInBackground(Void... params) {
+ return DBReader.getNavDrawerData(AudioplayerActivity.this);
+ }
+
+ @Override
+ protected void onPostExecute(DBReader.NavDrawerData result) {
+ super.onPostExecute(result);
+ navDrawerData = result;
+ if (navAdapter != null) {
+ navAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+ loadTask.execute();
+ }
+
+ private void cancelLoadTask() {
+ if (loadTask != null) {
+ loadTask.cancel(true);
+ }
+ }
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EventDistributor.FEED_LIST_UPDATE & arg) != 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received contentUpdate Intent.");
+ loadData();
+ }
+ }
+ };
+
+ private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ if (navDrawerData != null) {
+ return navDrawerData.feeds.size();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public Feed getItem(int position) {
+ if (navDrawerData != null && position < navDrawerData.feeds.size()) {
+ return navDrawerData.feeds.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getSelectedItemIndex() {
+ return -1;
+ }
+
+ @Override
+ public int getQueueSize() {
+ return (navDrawerData != null) ? navDrawerData.queueSize : 0;
+ }
+
+ @Override
+ public int getNumberOfUnreadItems() {
+ return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0;
+ }
+ };
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java
new file mode 100644
index 000000000..5e3817796
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java
@@ -0,0 +1,248 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.NavUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.examples.HtmlToPlainText;
+import org.jsoup.nodes.Document;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+
+/**
+ * Default implementation of OnlineFeedViewActivity. Shows the downloaded feed's items with their descriptions,
+ * a subscribe button and a spinner for choosing alternate feed URLs.
+ */
+public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity {
+ private static final String TAG = "DefaultOnlineFeedViewActivity";
+
+ private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.FEED_LIST_UPDATE;
+ private volatile List<Feed> feeds;
+ private Feed feed;
+ private String selectedDownloadUrl;
+
+ private Button subscribeButton;
+
+ @Override
+ protected void onCreate(Bundle arg0) {
+ super.onCreate(arg0);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ Intent destIntent = new Intent(this, MainActivity.class);
+ if (NavUtils.shouldUpRecreateTask(this, destIntent)) {
+ startActivity(destIntent);
+ } else {
+ NavUtils.navigateUpFromSameTask(this);
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void loadData() {
+ super.loadData();
+ feeds = DBReader.getFeedList(this);
+ }
+
+ @Override
+ protected void beforeShowFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) {
+ super.beforeShowFeedInformation(feed, alternateFeedUrls);
+
+ // remove HTML tags from descriptions
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Removing HTML from shownotes");
+ if (feed.getItems() != null) {
+ HtmlToPlainText formatter = new HtmlToPlainText();
+ for (FeedItem item : feed.getItems()) {
+ if (item.getDescription() != null) {
+ Document description = Jsoup.parse(item.getDescription());
+ item.setDescription(StringUtils.trim(formatter.getPlainText(description)));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void showFeedInformation(final Feed feed, final Map<String, String> alternateFeedUrls) {
+ super.showFeedInformation(feed, alternateFeedUrls);
+ setContentView(R.layout.listview_activity);
+
+ this.feed = feed;
+ this.selectedDownloadUrl = feed.getDownload_url();
+ EventDistributor.getInstance().register(listener);
+ ListView listView = (ListView) findViewById(R.id.listview);
+ LayoutInflater inflater = (LayoutInflater)
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View header = inflater.inflate(R.layout.onlinefeedview_header, listView, false);
+ listView.addHeaderView(header);
+
+ listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems()));
+
+ ImageView cover = (ImageView) header.findViewById(R.id.imgvCover);
+ TextView title = (TextView) header.findViewById(R.id.txtvTitle);
+ TextView author = (TextView) header.findViewById(R.id.txtvAuthor);
+ TextView description = (TextView) header.findViewById(R.id.txtvDescription);
+ Spinner spAlternateUrls = (Spinner) header.findViewById(R.id.spinnerAlternateUrls);
+
+ subscribeButton = (Button) header.findViewById(R.id.butSubscribe);
+
+ if (feed.getImage() != null && StringUtils.isNoneBlank(feed.getImage().getDownload_url())) {
+ PicassoProvider.getDefaultPicassoInstance(this)
+ .load(feed.getImage().getDownload_url())
+ .fit()
+ .into(cover);
+ }
+
+ title.setText(feed.getTitle());
+ author.setText(feed.getAuthor());
+ description.setText(feed.getDescription());
+
+ subscribeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ Feed f = new Feed(selectedDownloadUrl, new Date(), feed.getTitle());
+ f.setPreferences(feed.getPreferences());
+ DefaultOnlineFeedViewActivity.this.feed = f;
+
+ DownloadRequester.getInstance().downloadFeed(
+ DefaultOnlineFeedViewActivity.this,
+ f);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DownloadRequestErrorDialogCreator.newRequestErrorDialog(DefaultOnlineFeedViewActivity.this,
+ e.getMessage());
+ }
+ setSubscribeButtonState(feed);
+ }
+ });
+
+ if (alternateFeedUrls.isEmpty()) {
+ spAlternateUrls.setVisibility(View.GONE);
+ } else {
+ spAlternateUrls.setVisibility(View.VISIBLE);
+
+ final List<String> alternateUrlsList = new ArrayList<String>();
+ final List<String> alternateUrlsTitleList = new ArrayList<String>();
+
+ alternateUrlsList.add(feed.getDownload_url());
+ alternateUrlsTitleList.add(feed.getTitle());
+
+
+ alternateUrlsList.addAll(alternateFeedUrls.keySet());
+ for (String url : alternateFeedUrls.keySet()) {
+ alternateUrlsTitleList.add(alternateFeedUrls.get(url));
+ }
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, alternateUrlsTitleList);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spAlternateUrls.setAdapter(adapter);
+ spAlternateUrls.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ selectedDownloadUrl = alternateUrlsList.get(position);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+ });
+
+
+ }
+ setSubscribeButtonState(feed);
+
+ }
+
+ private boolean feedInFeedlist(Feed feed) {
+ if (feeds == null || feed == null)
+ return false;
+ for (Feed f : feeds) {
+ if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void setSubscribeButtonState(Feed feed) {
+ if (subscribeButton != null && feed != null) {
+ if (DownloadRequester.getInstance().isDownloadingFile(feed.getDownload_url())) {
+ subscribeButton.setEnabled(false);
+ subscribeButton.setText(R.string.downloading_label);
+ } else if (feedInFeedlist(feed)) {
+ subscribeButton.setEnabled(false);
+ subscribeButton.setText(R.string.subscribed_label);
+ } else {
+ subscribeButton.setEnabled(true);
+ subscribeButton.setText(R.string.subscribe_label);
+ }
+ }
+ }
+
+ EventDistributor.EventListener listener = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EventDistributor.FEED_LIST_UPDATE) != 0) {
+ new AsyncTask<Void, Void, List<Feed>>() {
+ @Override
+ protected List<Feed> doInBackground(Void... params) {
+ return DBReader.getFeedList(DefaultOnlineFeedViewActivity.this);
+ }
+
+ @Override
+ protected void onPostExecute(List<Feed> feeds) {
+ super.onPostExecute(feeds);
+ DefaultOnlineFeedViewActivity.this.feeds = feeds;
+ setSubscribeButtonState(feed);
+ }
+ }.execute();
+ } else if ((arg & EVENTS) != 0) {
+ setSubscribeButtonState(feed);
+ }
+ }
+ };
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(listener);
+ }
+}
+
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
new file mode 100644
index 000000000..559fa0574
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
@@ -0,0 +1,370 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileObserver;
+import android.support.v4.app.NavUtils;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.*;
+import android.widget.AdapterView.OnItemClickListener;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Let's the user choose a directory on the storage device. The selected folder
+ * will be sent back to the starting activity as an activity result.
+ */
+public class DirectoryChooserActivity extends ActionBarActivity {
+ private static final String TAG = "DirectoryChooserActivity";
+
+ private static final String CREATE_DIRECTORY_NAME = "AntennaPod";
+
+ public static final String RESULT_SELECTED_DIR = "selected_dir";
+ public static final int RESULT_CODE_DIR_SELECTED = 1;
+
+ private Button butConfirm;
+ private Button butCancel;
+ private ImageButton butNavUp;
+ private TextView txtvSelectedFolder;
+ private ListView listDirectories;
+
+ private ArrayAdapter<String> listDirectoriesAdapter;
+ private ArrayList<String> filenames;
+ /** The directory that is currently being shown. */
+ private File selectedDir;
+ private File[] filesInDir;
+
+ private FileObserver fileObserver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setContentView(R.layout.directory_chooser);
+ butConfirm = (Button) findViewById(R.id.butConfirm);
+ butCancel = (Button) findViewById(R.id.butCancel);
+ butNavUp = (ImageButton) findViewById(R.id.butNavUp);
+ txtvSelectedFolder = (TextView) findViewById(R.id.txtvSelectedFolder);
+ listDirectories = (ListView) findViewById(R.id.directory_list);
+
+ butConfirm.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isValidFile(selectedDir)) {
+ if (selectedDir.list().length == 0) {
+ returnSelectedFolder();
+ } else {
+ showNonEmptyDirectoryWarning();
+ }
+ }
+ }
+
+ private void showNonEmptyDirectoryWarning() {
+ AlertDialog.Builder adb = new AlertDialog.Builder(
+ DirectoryChooserActivity.this);
+ adb.setTitle(R.string.folder_not_empty_dialog_title);
+ adb.setMessage(R.string.folder_not_empty_dialog_msg);
+ adb.setNegativeButton(R.string.cancel_label,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ dialog.dismiss();
+ }
+ });
+ adb.setPositiveButton(R.string.confirm_label,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ dialog.dismiss();
+ returnSelectedFolder();
+ }
+ });
+ adb.create().show();
+ }
+ });
+
+ butCancel.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+ });
+
+ listDirectories.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> adapter, View view,
+ int position, long id) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Selected index: " + position);
+ if (filesInDir != null && position >= 0
+ && position < filesInDir.length) {
+ changeDirectory(filesInDir[position]);
+ }
+ }
+ });
+
+ butNavUp.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ File parent = null;
+ if (selectedDir != null
+ && (parent = selectedDir.getParentFile()) != null) {
+ changeDirectory(parent);
+ }
+ }
+ });
+
+ filenames = new ArrayList<String>();
+ listDirectoriesAdapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_list_item_1, filenames);
+ listDirectories.setAdapter(listDirectoriesAdapter);
+ changeDirectory(Environment.getExternalStorageDirectory());
+ }
+
+ /**
+ * Finishes the activity and returns the selected folder as a result. The
+ * selected folder can also be null.
+ */
+ private void returnSelectedFolder() {
+ if (selectedDir != null && BuildConfig.DEBUG)
+ Log.d(TAG, "Returning " + selectedDir.getAbsolutePath()
+ + " as result");
+ Intent resultData = new Intent();
+ if (selectedDir != null) {
+ resultData.putExtra(RESULT_SELECTED_DIR,
+ selectedDir.getAbsolutePath());
+ }
+ setResult(RESULT_CODE_DIR_SELECTED, resultData);
+ finish();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (fileObserver != null) {
+ fileObserver.stopWatching();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (fileObserver != null) {
+ fileObserver.startWatching();
+ }
+ }
+
+ /**
+ * Change the directory that is currently being displayed.
+ *
+ * @param dir
+ * The file the activity should switch to. This File must be
+ * non-null and a directory, otherwise the displayed directory
+ * will not be changed
+ */
+ private void changeDirectory(File dir) {
+ if (dir != null && dir.isDirectory()) {
+ File[] contents = dir.listFiles();
+ if (contents != null) {
+ int numDirectories = 0;
+ for (File f : contents) {
+ if (f.isDirectory()) {
+ numDirectories++;
+ }
+ }
+ filesInDir = new File[numDirectories];
+ filenames.clear();
+ for (int i = 0, counter = 0; i < numDirectories; counter++) {
+ if (contents[counter].isDirectory()) {
+ filesInDir[i] = contents[counter];
+ filenames.add(contents[counter].getName());
+ i++;
+ }
+ }
+ Arrays.sort(filesInDir);
+ Collections.sort(filenames);
+ selectedDir = dir;
+ txtvSelectedFolder.setText(dir.getAbsolutePath());
+ listDirectoriesAdapter.notifyDataSetChanged();
+ fileObserver = createFileObserver(dir.getAbsolutePath());
+ fileObserver.startWatching();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Changed directory to " + dir.getAbsolutePath());
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Could not change folder: contents of dir were null");
+ }
+ } else {
+ if (dir == null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Could not change folder: dir was null");
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Could not change folder: dir is no directory");
+ }
+ }
+ refreshButtonState();
+ }
+
+ /**
+ * Changes the state of the buttons depending on the currently selected file
+ * or folder.
+ */
+ private void refreshButtonState() {
+ if (selectedDir != null) {
+ butConfirm.setEnabled(isValidFile(selectedDir));
+ supportInvalidateOptionsMenu();
+ }
+ }
+
+ /** Refresh the contents of the directory that is currently shown. */
+ private void refreshDirectory() {
+ if (selectedDir != null) {
+ changeDirectory(selectedDir);
+ }
+ }
+
+ /** Sets up a FileObserver to watch the current directory. */
+ private FileObserver createFileObserver(String path) {
+ return new FileObserver(path, FileObserver.CREATE | FileObserver.DELETE
+ | FileObserver.MOVED_FROM | FileObserver.MOVED_TO) {
+
+ @Override
+ public void onEvent(int event, String path) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "FileObserver received event " + event);
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ refreshDirectory();
+ }
+ });
+ }
+ };
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.new_folder_item)
+ .setVisible(isValidFile(selectedDir));
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.directory_chooser, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ case R.id.new_folder_item:
+ openNewFolderDialog();
+ return true;
+ case R.id.set_to_default_folder_item:
+ selectedDir = null;
+ returnSelectedFolder();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Shows a confirmation dialog that asks the user if he wants to create a
+ * new folder.
+ */
+ private void openNewFolderDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.create_folder_label);
+ builder.setMessage(String.format(getString(R.string.create_folder_msg),
+ CREATE_DIRECTORY_NAME));
+ builder.setNegativeButton(R.string.cancel_label,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ builder.setPositiveButton(R.string.confirm_label,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ int msg = createFolder();
+ Toast t = Toast.makeText(DirectoryChooserActivity.this,
+ msg, Toast.LENGTH_SHORT);
+ t.show();
+ }
+ });
+ builder.create().show();
+ }
+
+ /**
+ * Creates a new folder in the current directory with the name
+ * CREATE_DIRECTORY_NAME.
+ */
+ private int createFolder() {
+ if (selectedDir == null) {
+ return R.string.create_folder_error;
+ } else if (selectedDir.canWrite()) {
+ File newDir = new File(selectedDir, CREATE_DIRECTORY_NAME);
+ if (!newDir.exists()) {
+ boolean result = newDir.mkdir();
+ if (result) {
+ return R.string.create_folder_success;
+ } else {
+ return R.string.create_folder_error;
+ }
+ } else {
+ return R.string.create_folder_error_already_exists;
+ }
+ } else {
+ return R.string.create_folder_error_no_write_access;
+ }
+ }
+
+ /** Returns true if the selected file or directory would be valid selection. */
+ private boolean isValidFile(File file) {
+ return (file != null && file.isDirectory() && file.canRead() && file
+ .canWrite());
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java
new file mode 100644
index 000000000..365c4216d
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java
@@ -0,0 +1,110 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+
+/**
+ * Shows a username and a password text field.
+ * The activity MUST be started with the ARG_DOWNlOAD_REQUEST argument set to a non-null value.
+ * Other arguments are optional.
+ * The activity's result will be the same DownloadRequest with the entered username and password.
+ */
+public class DownloadAuthenticationActivity extends ActionBarActivity {
+ private static final String TAG = "DownloadAuthenticationActivity";
+
+ /**
+ * The download request object that contains information about the resource that requires a username and a password
+ */
+ public static final String ARG_DOWNLOAD_REQUEST = "request";
+ /**
+ * True if the request should be sent to the DownloadRequester when this activity is finished, false otherwise.
+ * The default value is false.
+ */
+ public static final String ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL = "send_to_downloadrequester";
+
+ public static final String RESULT_REQUEST = "request";
+
+ private EditText etxtUsername;
+ private EditText etxtPassword;
+ private Button butConfirm;
+ private Button butCancel;
+ private TextView txtvDescription;
+
+ private DownloadRequest request;
+ private boolean sendToDownloadRequester;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().hide();
+ setContentView(R.layout.download_authentication_activity);
+
+ etxtUsername = (EditText) findViewById(R.id.etxtUsername);
+ etxtPassword = (EditText) findViewById(R.id.etxtPassword);
+ butConfirm = (Button) findViewById(R.id.butConfirm);
+ butCancel = (Button) findViewById(R.id.butCancel);
+ txtvDescription = (TextView) findViewById(R.id.txtvDescription);
+
+ Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing");
+
+ request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST);
+ sendToDownloadRequester = getIntent().getBooleanExtra(ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, false);
+
+ if (savedInstanceState != null) {
+ etxtUsername.setText(savedInstanceState.getString("username"));
+ etxtPassword.setText(savedInstanceState.getString("password"));
+ }
+
+ txtvDescription.setText(txtvDescription.getText() + ":\n\n" + request.getTitle());
+
+ butCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+ });
+
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String username = etxtUsername.getText().toString();
+ String password = etxtPassword.getText().toString();
+ request.setUsername(username);
+ request.setPassword(password);
+ Intent result = new Intent();
+ result.putExtra(RESULT_REQUEST, request);
+ setResult(Activity.RESULT_OK, result);
+
+ if (sendToDownloadRequester) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Sending request to DownloadRequester");
+ DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request);
+ }
+ finish();
+ }
+ });
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString("username", etxtUsername.getText().toString());
+ outState.putString("password", etxtPassword.getText().toString());
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java
new file mode 100644
index 000000000..3000cfaeb
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java
@@ -0,0 +1,192 @@
+package de.danoeh.antennapod.activity;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.*;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.util.LangUtils;
+import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
+
+/**
+ * Displays information about a feed.
+ */
+public class FeedInfoActivity extends ActionBarActivity {
+ private static final String TAG = "FeedInfoActivity";
+
+ public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
+
+ private Feed feed;
+
+ private ImageView imgvCover;
+ private TextView txtvTitle;
+ private TextView txtvDescription;
+ private TextView txtvLanguage;
+ private TextView txtvAuthor;
+ private EditText etxtUsername;
+ private EditText etxtPassword;
+ private CheckBox cbxAutoDownload;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.feedinfo);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1);
+
+ imgvCover = (ImageView) findViewById(R.id.imgvCover);
+ txtvTitle = (TextView) findViewById(R.id.txtvTitle);
+ txtvDescription = (TextView) findViewById(R.id.txtvDescription);
+ txtvLanguage = (TextView) findViewById(R.id.txtvLanguage);
+ txtvAuthor = (TextView) findViewById(R.id.txtvAuthor);
+ cbxAutoDownload = (CheckBox) findViewById(R.id.cbxAutoDownload);
+ etxtUsername = (EditText) findViewById(R.id.etxtUsername);
+ etxtPassword = (EditText) findViewById(R.id.etxtPassword);
+
+ AsyncTask<Long, Void, Feed> loadTask = new AsyncTask<Long, Void, Feed>() {
+
+ @Override
+ protected Feed doInBackground(Long... params) {
+ return DBReader.getFeed(FeedInfoActivity.this, params[0]);
+ }
+
+ @Override
+ protected void onPostExecute(Feed result) {
+ if (result != null) {
+ feed = result;
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Language is " + feed.getLanguage());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Author is " + feed.getAuthor());
+ imgvCover.post(new Runnable() {
+
+ @Override
+ public void run() {
+ PicassoProvider.getDefaultPicassoInstance(FeedInfoActivity.this)
+ .load(feed.getImageUri())
+ .fit()
+ .into(imgvCover);
+ }
+ });
+
+ txtvTitle.setText(feed.getTitle());
+ txtvDescription.setText(feed.getDescription());
+ if (feed.getAuthor() != null) {
+ txtvAuthor.setText(feed.getAuthor());
+ }
+ if (feed.getLanguage() != null) {
+ txtvLanguage.setText(LangUtils
+ .getLanguageString(feed.getLanguage()));
+ }
+
+ cbxAutoDownload.setEnabled(UserPreferences.isEnableAutodownload());
+ cbxAutoDownload.setChecked(feed.getPreferences().getAutoDownload());
+ cbxAutoDownload.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
+ feed.getPreferences().setAutoDownload(checked);
+ feed.savePreferences(FeedInfoActivity.this);
+ }
+ });
+
+ etxtUsername.setText(feed.getPreferences().getUsername());
+ etxtPassword.setText(feed.getPreferences().getPassword());
+
+ etxtUsername.addTextChangedListener(authTextWatcher);
+ etxtPassword.addTextChangedListener(authTextWatcher);
+
+ supportInvalidateOptionsMenu();
+
+ } else {
+ Log.e(TAG, "Activity was started with invalid arguments");
+ }
+ }
+ };
+ loadTask.execute(feedId);
+ }
+
+
+ private boolean authInfoChanged = false;
+
+ private TextWatcher authTextWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ authInfoChanged = true;
+ }
+ };
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (feed != null && authInfoChanged) {
+ Log.d(TAG, "Auth info changed, saving credentials");
+ FeedPreferences prefs = feed.getPreferences();
+ prefs.setUsername(etxtUsername.getText().toString());
+ prefs.setPassword(etxtPassword.getText().toString());
+ DBWriter.setFeedPreferences(this, prefs);
+ authInfoChanged = false;
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.feedinfo, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.support_item).setVisible(
+ feed != null && feed.getPaymentLink() != null);
+ menu.findItem(R.id.share_link_item).setVisible(feed != null &&feed.getLink() != null);
+ menu.findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ try {
+ return FeedMenuHandler.onOptionsItemClicked(this, item, feed);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DownloadRequestErrorDialogCreator.newRequestErrorDialog(this,
+ e.getMessage());
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java
new file mode 100644
index 000000000..f4a973fac
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java
@@ -0,0 +1,125 @@
+package de.danoeh.antennapod.activity;
+
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.TextView;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import org.shredzone.flattr4j.exception.FlattrException;
+
+/** Guides the user through the authentication process */
+
+public class FlattrAuthActivity extends ActionBarActivity {
+ private static final String TAG = "FlattrAuthActivity";
+
+ private TextView txtvExplanation;
+ private Button butAuthenticate;
+ private Button butReturn;
+
+ private boolean authSuccessful;
+
+ private static FlattrAuthActivity singleton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+ singleton = this;
+ authSuccessful = false;
+ if (BuildConfig.DEBUG) Log.d(TAG, "Activity created");
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.flattr_auth);
+ txtvExplanation = (TextView) findViewById(R.id.txtvExplanation);
+ butAuthenticate = (Button) findViewById(R.id.but_authenticate);
+ butReturn = (Button) findViewById(R.id.but_return_home);
+
+ butReturn.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(FlattrAuthActivity.this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+ });
+
+ butAuthenticate.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ FlattrUtils.startAuthProcess(FlattrAuthActivity.this);
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ public static FlattrAuthActivity getInstance() {
+ return singleton;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (BuildConfig.DEBUG) Log.d(TAG, "Activity resumed");
+ Uri uri = getIntent().getData();
+ if (uri != null) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Received uri");
+ FlattrUtils.handleCallback(this, uri);
+ }
+ }
+
+ public void handleAuthenticationSuccess() {
+ authSuccessful = true;
+ txtvExplanation.setText(R.string.flattr_auth_success);
+ butAuthenticate.setEnabled(false);
+ butReturn.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return true;
+ }
+
+
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (authSuccessful) {
+ finish();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (authSuccessful) {
+ Intent intent = new Intent(this, PreferenceActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ } else {
+ finish();
+ }
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
new file mode 100644
index 000000000..7029fd32c
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -0,0 +1,432 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.media.AudioManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.*;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.NavListAdapter;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.fragment.*;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+import java.util.List;
+
+/**
+ * The activity that is shown when the user launches the app.
+ */
+public class MainActivity extends ActionBarActivity implements NavDrawerActivity{
+ private static final String TAG = "MainActivity";
+ private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED
+ | EventDistributor.DOWNLOAD_QUEUED
+ | EventDistributor.FEED_LIST_UPDATE
+ | EventDistributor.UNREAD_ITEMS_UPDATE
+ | EventDistributor.QUEUE_UPDATE;
+
+ public static final String PREF_NAME = "MainActivityPrefs";
+ public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch";
+
+ public static final String EXTRA_NAV_INDEX = "nav_index";
+ public static final String EXTRA_NAV_TYPE = "nav_type";
+ public static final String EXTRA_FRAGMENT_ARGS = "fragment_args";
+
+ public static final int POS_NEW = 0,
+ POS_QUEUE = 1,
+ POS_DOWNLOADS = 2,
+ POS_HISTORY = 3,
+ POS_ADD = 4;
+
+ private ExternalPlayerFragment externalPlayerFragment;
+ private DrawerLayout drawerLayout;
+
+ private ListView navList;
+ private NavListAdapter navAdapter;
+
+ private ActionBarDrawerToggle drawerToggle;
+
+ private CharSequence drawerTitle;
+ private CharSequence currentTitle;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+ supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ StorageUtils.checkStorageAvailability(this);
+ setContentView(R.layout.main);
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ drawerTitle = currentTitle = getTitle();
+
+ drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ navList = (ListView) findViewById(R.id.nav_list);
+
+ TypedArray typedArray = obtainStyledAttributes(new int[]{R.attr.nav_drawer_toggle});
+ drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, typedArray.getResourceId(0, 0), R.string.drawer_open, R.string.drawer_close) {
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ currentTitle = getSupportActionBar().getTitle();
+ getSupportActionBar().setTitle(drawerTitle);
+ supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ super.onDrawerClosed(drawerView);
+ getSupportActionBar().setTitle(currentTitle);
+ supportInvalidateOptionsMenu();
+
+ }
+ };
+ typedArray.recycle();
+
+ drawerLayout.setDrawerListener(drawerToggle);
+ FragmentManager fm = getSupportFragmentManager();
+
+ FragmentTransaction transaction = fm.beginTransaction();
+
+ Fragment mainFragment = fm.findFragmentByTag("main");
+ if (mainFragment != null) {
+ transaction.replace(R.id.main_view, mainFragment);
+ } else {
+ loadFragment(NavListAdapter.VIEW_TYPE_NAV, POS_NEW, null);
+ }
+
+ externalPlayerFragment = new ExternalPlayerFragment();
+ transaction.replace(R.id.playerFragment, externalPlayerFragment);
+ transaction.commit();
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ navAdapter = new NavListAdapter(itemAccess, this);
+ navList.setAdapter(navAdapter);
+ navList.setOnItemClickListener(navListClickListener);
+
+ checkFirstLaunch();
+ }
+
+ private void checkFirstLaunch() {
+ SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) {
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ drawerLayout.openDrawer(navList);
+ }
+ }, 1500);
+
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putBoolean(PREF_IS_FIRST_LAUNCH, false);
+ edit.commit();
+ }
+ }
+
+ public ActionBar getMainActivtyActionBar() {
+ return getSupportActionBar();
+ }
+
+ public boolean isDrawerOpen() {
+ return drawerLayout != null && navList != null && drawerLayout.isDrawerOpen(navList);
+ }
+
+ public List<Feed> getFeeds() {
+ return (navDrawerData != null) ? navDrawerData.feeds : null;
+ }
+
+ private void loadFragment(int viewType, int relPos, Bundle args) {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ // clear back stack
+ for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) {
+ fragmentManager.popBackStack();
+ }
+
+ FragmentTransaction fT = fragmentManager.beginTransaction();
+ Fragment fragment = null;
+ if (viewType == NavListAdapter.VIEW_TYPE_NAV) {
+ switch (relPos) {
+ case POS_NEW:
+ fragment = new NewEpisodesFragment();
+ break;
+ case POS_QUEUE:
+ fragment = new QueueFragment();
+ break;
+ case POS_DOWNLOADS:
+ fragment = new DownloadsFragment();
+ break;
+ case POS_HISTORY:
+ fragment = new PlaybackHistoryFragment();
+ break;
+ case POS_ADD:
+ fragment = new AddFeedFragment();
+ break;
+
+ }
+ currentTitle = getString(NavListAdapter.NAV_TITLES[relPos]);
+ selectedNavListIndex = relPos;
+
+ } else if (viewType == NavListAdapter.VIEW_TYPE_SUBSCRIPTION) {
+ Feed feed = itemAccess.getItem(relPos);
+ currentTitle = "";
+ fragment = ItemlistFragment.newInstance(feed.getId());
+ selectedNavListIndex = NavListAdapter.SUBSCRIPTION_OFFSET + relPos;
+
+ }
+ if (fragment != null) {
+ if (args != null) {
+ fragment.setArguments(args);
+ }
+ fT.replace(R.id.main_view, fragment, "main");
+ fragmentManager.popBackStack();
+ }
+ fT.commit();
+ getSupportActionBar().setTitle(currentTitle);
+ if (navAdapter != null) {
+ navAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void loadNavFragment(int position, Bundle args) {
+ loadFragment(NavListAdapter.VIEW_TYPE_NAV, position, args);
+ }
+
+ public void loadFeedFragment(long feedID) {
+ if (navDrawerData != null) {
+ for (int i = 0; i < navDrawerData.feeds.size(); i++) {
+ if (navDrawerData.feeds.get(i).getId() == feedID) {
+ loadFragment(NavListAdapter.VIEW_TYPE_SUBSCRIPTION, i, null);
+ break;
+ }
+ }
+ }
+ }
+
+ public void loadChildFragment(Fragment fragment) {
+ Validate.notNull(fragment);
+ FragmentManager fm = getSupportFragmentManager();
+ fm.beginTransaction()
+ .replace(R.id.main_view, fragment, "main")
+ .addToBackStack(null)
+ .commit();
+ }
+
+ private AdapterView.OnItemClickListener navListClickListener = new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ int viewType = parent.getAdapter().getItemViewType(position);
+ if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER && position != selectedNavListIndex) {
+ int relPos = (viewType == NavListAdapter.VIEW_TYPE_NAV) ? position : position - NavListAdapter.SUBSCRIPTION_OFFSET;
+ loadFragment(viewType, relPos, null);
+ selectedNavListIndex = position;
+ navAdapter.notifyDataSetChanged();
+ }
+ drawerLayout.closeDrawer(navList);
+ }
+ };
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ drawerToggle.syncState();
+ if (savedInstanceState != null) {
+ currentTitle = savedInstanceState.getString("title");
+ if (!drawerLayout.isDrawerOpen(navList)) {
+ getSupportActionBar().setTitle(currentTitle);
+ }
+ selectedNavListIndex = savedInstanceState.getInt("selectedNavIndex");
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ drawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString("title", getSupportActionBar().getTitle().toString());
+ outState.putInt("selectedNavIndex", selectedNavListIndex);
+
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ StorageUtils.checkStorageAvailability(this);
+ EventDistributor.getInstance().register(contentUpdate);
+
+ Intent intent = getIntent();
+ if (navDrawerData != null && intent.hasExtra(EXTRA_NAV_INDEX) && intent.hasExtra(EXTRA_NAV_TYPE)) {
+ handleNavIntent();
+ }
+
+ loadData();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ cancelLoadTask();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (drawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+ switch (item.getItemId()) {
+ case R.id.show_preferences:
+ startActivity(new Intent(this, PreferenceActivity.class));
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main, menu);
+ return true;
+ }
+
+ private DBReader.NavDrawerData navDrawerData;
+ private AsyncTask<Void, Void, DBReader.NavDrawerData> loadTask;
+ private int selectedNavListIndex = 0;
+
+ private NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ if (navDrawerData != null) {
+ return navDrawerData.feeds.size();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public Feed getItem(int position) {
+ if (navDrawerData != null && position < navDrawerData.feeds.size()) {
+ return navDrawerData.feeds.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getSelectedItemIndex() {
+ return selectedNavListIndex;
+ }
+
+ @Override
+ public int getQueueSize() {
+ return (navDrawerData != null) ? navDrawerData.queueSize : 0;
+ }
+
+ @Override
+ public int getNumberOfUnreadItems() {
+ return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0;
+ }
+
+
+ };
+
+ private void loadData() {
+ cancelLoadTask();
+ loadTask = new AsyncTask<Void, Void, DBReader.NavDrawerData>() {
+ @Override
+ protected DBReader.NavDrawerData doInBackground(Void... params) {
+ return DBReader.getNavDrawerData(MainActivity.this);
+ }
+
+ @Override
+ protected void onPostExecute(DBReader.NavDrawerData result) {
+ super.onPostExecute(navDrawerData);
+ boolean handleIntent = (navDrawerData == null);
+
+ navDrawerData = result;
+ navAdapter.notifyDataSetChanged();
+
+ if (handleIntent) {
+ handleNavIntent();
+ }
+ }
+ };
+ loadTask.execute();
+ }
+
+ private void cancelLoadTask() {
+ if (loadTask != null) {
+ loadTask.cancel(true);
+ }
+ }
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EVENTS & arg) != 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received contentUpdate Intent.");
+ loadData();
+ }
+ }
+ };
+
+ private void handleNavIntent() {
+ Intent intent = getIntent();
+ if (intent.hasExtra(EXTRA_NAV_INDEX) && intent.hasExtra(EXTRA_NAV_TYPE)) {
+ int index = intent.getIntExtra(EXTRA_NAV_INDEX, 0);
+ int type = intent.getIntExtra(EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV);
+ Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS);
+ loadFragment(type, index, args);
+ }
+ setIntent(new Intent(MainActivity.this, MainActivity.class)); // to avoid handling the intent twice when the configuration changes
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
new file mode 100644
index 000000000..14cb2727f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -0,0 +1,522 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.Window;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+import com.doomonafireball.betterpickers.hmspicker.HmsPickerBuilder;
+import com.doomonafireball.betterpickers.hmspicker.HmsPickerDialogFragment;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.dialog.TimeDialog;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.ShareUtils;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlaybackController;
+
+/**
+ * Provides general features which are both needed for playing audio and video
+ * 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 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();
+ }
+
+ @Override
+ protected void setScreenOn(boolean enable) {
+ super.setScreenOn(enable);
+ MediaplayerActivity.this.setScreenOn(enable);
+ }
+ };
+
+ }
+
+ protected void onPlaybackSpeedChange() {
+
+ }
+
+ protected void onServiceQueried() {
+ supportInvalidateOptionsMenu();
+ }
+
+ protected void chooseTheme() {
+ setTheme(UserPreferences.getTheme());
+ }
+
+ protected void setScreenOn(boolean enable) {
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ chooseTheme();
+ super.onCreate(savedInstanceState);
+
+ // subclasses might use this feature
+ supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ if (BuildConfig.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 (BuildConfig.DEBUG)
+ Log.d(TAG, "Activity stopped");
+ if (controller != null) {
+ controller.release();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (BuildConfig.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;
+ }
+
+ @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 &&
+ (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()) {
+ int pickerStyle = (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Light) ?
+ R.style.AntennaPodBetterPickerThemeLight : R.style.AntennaPodBetterPickerThemeDark;
+ if (Build.VERSION.SDK_INT > 10) { // TODO remove this as soon as dialog is shown correctly on 2.3
+ HmsPickerBuilder hpb = new HmsPickerBuilder()
+ .setStyleResId(pickerStyle)
+ .setFragmentManager(getSupportFragmentManager());
+
+ hpb.addHmsPickerDialogHandler(new HmsPickerDialogFragment.HmsPickerDialogHandler() {
+ @Override
+ public void onDialogHmsSet(int ref, int hours, int minutes, int seconds) {
+ if (controller != null && controller.serviceAvailable()) {
+ controller.setSleepTimer((hours * 3600 + minutes * 60 + seconds) * 1000);
+ }
+ }
+ });
+ hpb.show();
+ } else {
+ 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 (BuildConfig.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 (BuildConfig.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 (BuildConfig.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) {
+ if (controller != null) {
+ prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser,
+ txtvPosition);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (controller != null) {
+ controller.onSeekBarStartTrackingTouch(seekBar);
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (controller != null) {
+ controller.onSeekBarStopTrackingTouch(seekBar, prog);
+ }
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
new file mode 100644
index 000000000..d84e6cc03
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
@@ -0,0 +1,428 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.dialog.AuthenticationDialog;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.service.download.HttpDownloader;
+import de.danoeh.antennapod.core.syndication.handler.FeedHandler;
+import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult;
+import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.core.util.FileNameGenerator;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.core.util.URLChecker;
+import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer;
+import org.apache.commons.lang3.StringUtils;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Downloads a feed from a feed URL and parses it. Subclasses can display the
+ * feed object that was parsed. This activity MUST be started with a given URL
+ * or an Exception will be thrown.
+ * <p/>
+ * If the feed cannot be downloaded or parsed, an error dialog will be displayed
+ * and the activity will finish as soon as the error dialog is closed.
+ */
+public abstract class OnlineFeedViewActivity extends ActionBarActivity {
+ private static final String TAG = "OnlineFeedViewActivity";
+ public static final String ARG_FEEDURL = "arg.feedurl";
+
+ /**
+ * Optional argument: specify a title for the actionbar.
+ */
+ public static final String ARG_TITLE = "title";
+
+ public static final int RESULT_ERROR = 2;
+
+ private Feed feed;
+ private Map<String, String> alternateFeedUrls;
+ private Downloader downloader;
+
+ private boolean isPaused;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+
+ if (getIntent() != null && getIntent().hasExtra(ARG_TITLE)) {
+ getSupportActionBar().setTitle(getIntent().getStringExtra(ARG_TITLE));
+ }
+
+ StorageUtils.checkStorageAvailability(this);
+
+ final String feedUrl;
+ if (getIntent().hasExtra(ARG_FEEDURL)) {
+ feedUrl = getIntent().getStringExtra(ARG_FEEDURL);
+ } else if (StringUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)
+ || StringUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
+ feedUrl = (StringUtils.equals(getIntent().getAction(), Intent.ACTION_SEND))
+ ? getIntent().getStringExtra(Intent.EXTRA_TEXT) : getIntent().getDataString();
+
+ getSupportActionBar().setTitle(R.string.add_new_feed_label);
+ } else {
+ throw new IllegalArgumentException(
+ "Activity must be started with feedurl argument!");
+ }
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Activity was started with url " + feedUrl);
+ setLoadingLayout();
+ if (savedInstanceState == null) {
+ startFeedDownload(feedUrl, null, null);
+ } else {
+ startFeedDownload(feedUrl, savedInstanceState.getString("username"), savedInstanceState.getString("password"));
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ isPaused = false;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ isPaused = true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (feed != null && feed.getPreferences() != null) {
+ outState.putString("username", feed.getPreferences().getUsername());
+ outState.putString("password", feed.getPreferences().getPassword());
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (downloader != null && !downloader.isFinished()) {
+ downloader.cancel();
+ }
+ }
+
+ private void resetIntent(String url, String title) {
+ Intent intent = new Intent();
+ intent.putExtra(ARG_FEEDURL, url);
+ intent.putExtra(ARG_TITLE, title);
+ setIntent(intent);
+ }
+
+
+ private void onDownloadCompleted(final Downloader downloader) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Download was completed");
+ DownloadStatus status = downloader.getResult();
+ if (status != null) {
+ if (!status.isCancelled()) {
+ if (status.isSuccessful()) {
+ parseFeed();
+ } else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) {
+ if (!isFinishing() && !isPaused) {
+ Dialog dialog = new FeedViewAuthenticationDialog(OnlineFeedViewActivity.this,
+ R.string.authentication_notification_title, downloader.getDownloadRequest().getSource());
+ dialog.show();
+ }
+ } else {
+ String errorMsg = status.getReason().getErrorString(
+ OnlineFeedViewActivity.this);
+ if (errorMsg != null
+ && status.getReasonDetailed() != null) {
+ errorMsg += " ("
+ + status.getReasonDetailed() + ")";
+ }
+ showErrorDialog(errorMsg);
+ }
+ }
+ } else {
+ Log.wtf(TAG,
+ "DownloadStatus returned by Downloader was null");
+ finish();
+ }
+ }
+ });
+
+ }
+
+ private void startFeedDownload(String url, String username, String password) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting feed download");
+ url = URLChecker.prepareURL(url);
+ feed = new Feed(url, new Date());
+ if (username != null && password != null) {
+ feed.setPreferences(new FeedPreferences(0, true, username, password));
+ }
+ String fileUrl = new File(getExternalCacheDir(),
+ FileNameGenerator.generateFileName(feed.getDownload_url()))
+ .toString();
+ feed.setFile_url(fileUrl);
+ final DownloadRequest request = new DownloadRequest(feed.getFile_url(),
+ feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED, username, password, true);
+ downloader = new HttpDownloader(
+ request);
+ new Thread() {
+ @Override
+ public void run() {
+ loadData();
+ downloader.call();
+ onDownloadCompleted(downloader);
+ }
+ }.start();
+
+
+ }
+
+ /**
+ * Displays a progress indicator.
+ */
+ private void setLoadingLayout() {
+ RelativeLayout rl = new RelativeLayout(this);
+ RelativeLayout.LayoutParams rlLayoutParams = new RelativeLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT);
+
+ ProgressBar pb = new ProgressBar(this);
+ pb.setIndeterminate(true);
+ RelativeLayout.LayoutParams pbLayoutParams = new RelativeLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ pbLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
+ rl.addView(pb, pbLayoutParams);
+ addContentView(rl, rlLayoutParams);
+ }
+
+ private void parseFeed() {
+ if (feed == null || feed.getFile_url() == null && feed.isDownloaded()) {
+ throw new IllegalStateException(
+ "feed must be non-null and downloaded when parseFeed is called");
+ }
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Parsing feed");
+
+ Thread thread = new Thread() {
+
+ @Override
+ public void run() {
+ String reasonDetailed = "";
+ boolean successful = false;
+ FeedHandler handler = new FeedHandler();
+ try {
+ FeedHandlerResult result = handler.parseFeed(feed);
+ feed = result.feed;
+ alternateFeedUrls = result.alternateFeedUrls;
+ successful = true;
+ } catch (SAXException e) {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ } catch (IOException e) {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ } catch (ParserConfigurationException e) {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ } catch (UnsupportedFeedtypeException e) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Unsupported feed type detected");
+ if (StringUtils.equalsIgnoreCase("html", e.getRootElement())) {
+ if (showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url())) {
+ return;
+ }
+ } else {
+ e.printStackTrace();
+ reasonDetailed = e.getMessage();
+ }
+ } finally {
+ boolean rc = new File(feed.getFile_url()).delete();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Deleted feed source file. Result: " + rc);
+ }
+
+ if (successful) {
+ beforeShowFeedInformation(feed, alternateFeedUrls);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showFeedInformation(feed, alternateFeedUrls);
+ }
+ });
+ } else {
+ final String errorMsg =
+ DownloadError.ERROR_PARSER_EXCEPTION.getErrorString(
+ OnlineFeedViewActivity.this)
+ + " (" + reasonDetailed + ")";
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ showErrorDialog(errorMsg);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ /**
+ * Can be used to load data asynchronously.
+ */
+ protected void loadData() {
+
+ }
+
+ /**
+ * Called after the feed has been downloaded and parsed and before showFeedInformation is called.
+ * This method is executed on a background thread
+ */
+ protected void beforeShowFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) {
+
+ }
+
+ /**
+ * Called when feed parsed successfully.
+ * This method is executed on the GUI thread.
+ */
+ protected void showFeedInformation(Feed feed, Map<String, String> alternateFeedUrls) {
+
+ }
+
+ private void showErrorDialog(String errorMsg) {
+ if (!isFinishing() && !isPaused) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.error_label);
+ if (errorMsg != null) {
+ builder.setMessage(getString(R.string.error_msg_prefix) + errorMsg);
+ } else {
+ builder.setMessage(R.string.error_msg_prefix);
+ }
+ builder.setNeutralButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ }
+ );
+ builder.setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ setResult(RESULT_ERROR);
+ finish();
+ }
+ });
+ builder.show();
+ }
+ }
+
+ private boolean showFeedDiscoveryDialog(File feedFile, String baseUrl) {
+ FeedDiscoverer fd = new FeedDiscoverer();
+ final Map<String, String> urlsMap;
+ try {
+ urlsMap = fd.findLinks(feedFile, baseUrl);
+ if (urlsMap == null || urlsMap.isEmpty()) {
+ return false;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (isPaused || isFinishing()) {
+ return;
+ }
+
+ final List<String> titles = new ArrayList<String>();
+ final List<String> urls = new ArrayList<String>();
+
+ urls.addAll(urlsMap.keySet());
+ for (String url : urls) {
+ titles.add(urlsMap.get(url));
+ }
+
+ final ArrayAdapter<String> adapter = new ArrayAdapter<String>(OnlineFeedViewActivity.this, R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles);
+ DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String selectedUrl = urls.get(which);
+ dialog.dismiss();
+ resetIntent(selectedUrl, titles.get(which));
+ startFeedDownload(selectedUrl, null, null);
+ }
+ };
+
+ AlertDialog.Builder ab = new AlertDialog.Builder(OnlineFeedViewActivity.this)
+ .setTitle(R.string.feeds_label)
+ .setCancelable(true)
+ .setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ })
+ .setAdapter(adapter, onClickListener);
+ ab.show();
+ }
+ });
+
+
+ return true;
+ }
+
+ private class FeedViewAuthenticationDialog extends AuthenticationDialog {
+
+ private String feedUrl;
+
+ public FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) {
+ super(context, titleRes, true, false, null, null);
+ this.feedUrl = feedUrl;
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ finish();
+ }
+
+ @Override
+ protected void onConfirmed(String username, String password, boolean saveUsernamePassword) {
+ startFeedDownload(feedUrl, username, password);
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java
new file mode 100644
index 000000000..8a9ec58d3
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java
@@ -0,0 +1,134 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.ActionBarActivity;
+import android.util.SparseBooleanArray;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.opml.OpmlElement;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays the feeds that the OPML-Importer has read and lets the user choose
+ * which feeds he wants to import.
+ */
+public class OpmlFeedChooserActivity extends ActionBarActivity {
+ private static final String TAG = "OpmlFeedChooserActivity";
+
+ public static final String EXTRA_SELECTED_ITEMS = "de.danoeh.antennapod.selectedItems";
+
+ private Button butConfirm;
+ private Button butCancel;
+ private ListView feedlist;
+ private ArrayAdapter<String> listAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.opml_selection);
+ butConfirm = (Button) findViewById(R.id.butConfirm);
+ butCancel = (Button) findViewById(R.id.butCancel);
+ feedlist = (ListView) findViewById(R.id.feedlist);
+
+ feedlist.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ listAdapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_list_item_multiple_choice,
+ getTitleList());
+
+ feedlist.setAdapter(listAdapter);
+
+ butCancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+
+ butConfirm.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent();
+ SparseBooleanArray checked = feedlist.getCheckedItemPositions();
+
+ int checkedCount = 0;
+ // Get number of checked items
+ for (int i = 0; i < checked.size(); i++) {
+ if (checked.valueAt(i)) {
+ checkedCount++;
+ }
+ }
+ int[] selection = new int[checkedCount];
+ for (int i = 0, collected = 0; collected < checkedCount; i++) {
+ if (checked.valueAt(i)) {
+ selection[collected] = checked.keyAt(i);
+ collected++;
+ }
+ }
+ intent.putExtra(EXTRA_SELECTED_ITEMS, selection);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+ });
+
+ }
+
+ private List<String> getTitleList() {
+ List<String> result = new ArrayList<String>();
+ if (OpmlImportHolder.getReadElements() != null) {
+ for (OpmlElement element : OpmlImportHolder.getReadElements()) {
+ result.add(element.getText());
+ }
+
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.select_all_item, Menu.NONE,
+ R.string.select_all_label),
+ MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+
+ MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.deselect_all_item, Menu.NONE,
+ R.string.deselect_all_label),
+ MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.select_all_item:
+ selectAllItems(true);
+ return true;
+ case R.id.deselect_all_item:
+ selectAllItems(false);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void selectAllItems(boolean b) {
+ for (int i = 0; i < feedlist.getCount(); i++) {
+ feedlist.setItemChecked(i, b);
+ }
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java
new file mode 100644
index 000000000..d974e0e1b
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java
@@ -0,0 +1,90 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.Intent;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.asynctask.OpmlFeedQueuer;
+import de.danoeh.antennapod.asynctask.OpmlImportWorker;
+import de.danoeh.antennapod.core.opml.OpmlElement;
+
+import java.io.Reader;
+import java.util.ArrayList;
+
+/**
+ * Base activity for Opml Import - e.g. with code what to do afterwards
+ * */
+public class OpmlImportBaseActivity extends ActionBarActivity {
+
+ private static final String TAG = "OpmlImportBaseActivity";
+ private OpmlImportWorker importWorker;
+
+ /**
+ * Handles the choices made by the user in the OpmlFeedChooserActivity and
+ * starts the OpmlFeedQueuer if necessary.
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received result");
+ if (resultCode == RESULT_CANCELED) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Activity was cancelled");
+ if (finishWhenCanceled())
+ finish();
+ } else {
+ int[] selected = data
+ .getIntArrayExtra(OpmlFeedChooserActivity.EXTRA_SELECTED_ITEMS);
+ if (selected != null && selected.length > 0) {
+ OpmlFeedQueuer queuer = new OpmlFeedQueuer(this, selected) {
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ Intent intent = new Intent(OpmlImportBaseActivity.this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+
+ };
+ queuer.executeAsync();
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "No items were selected");
+ }
+ }
+ }
+
+ /** Starts the import process. */
+ protected void startImport(Reader reader) {
+
+ if (reader != null) {
+ importWorker = new OpmlImportWorker(this, reader) {
+
+ @Override
+ protected void onPostExecute(ArrayList<OpmlElement> result) {
+ super.onPostExecute(result);
+ if (result != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Parsing was successful");
+ OpmlImportHolder.setReadElements(result);
+ startActivityForResult(new Intent(
+ OpmlImportBaseActivity.this,
+ OpmlFeedChooserActivity.class), 0);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Parser error occurred");
+ }
+ }
+ };
+ importWorker.executeAsync();
+ }
+ }
+
+ protected boolean finishWhenCanceled() {
+ return false;
+ }
+
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
new file mode 100644
index 000000000..e42072ead
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
@@ -0,0 +1,38 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.LangUtils;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.URL;
+
+/** Lets the user start the OPML-import process. */
+public class OpmlImportFromIntentActivity extends OpmlImportBaseActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ try {
+ URL mOpmlURL = new URL(getIntent().getData().toString());
+ BufferedReader in = new BufferedReader(new InputStreamReader(mOpmlURL.openStream(),
+ LangUtils.UTF_8));
+ startImport(in);
+ } catch (Exception e) {
+ new AlertDialog.Builder(this).setMessage("Cannot open XML - Reason: " + e.getMessage()).show();
+ }
+
+ }
+
+ @Override
+ protected boolean finishWhenCanceled() {
+ return true;
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
new file mode 100644
index 000000000..162a8f2e5
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
@@ -0,0 +1,171 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.LangUtils;
+import de.danoeh.antennapod.core.util.StorageUtils;
+
+import java.io.*;
+
+/**
+ * Lets the user start the OPML-import process from a path
+ */
+public class OpmlImportFromPathActivity extends OpmlImportBaseActivity {
+ private static final String TAG = "OpmlImportFromPathActivity";
+ private TextView txtvPath;
+ private Button butStart;
+ private String importPath;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.opml_import);
+
+ txtvPath = (TextView) findViewById(R.id.txtvPath);
+ butStart = (Button) findViewById(R.id.butStartImport);
+
+ butStart.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ checkFolderForFiles();
+ }
+
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ StorageUtils.checkStorageAvailability(this);
+ setImportPath();
+ }
+
+ /**
+ * Sets the importPath variable and makes txtvPath display the import
+ * directory.
+ */
+ private void setImportPath() {
+ File importDir = UserPreferences.getDataFolder(this, UserPreferences.IMPORT_DIR);
+ boolean success = true;
+ if (!importDir.exists()) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Import directory doesn't exist. Creating...");
+ success = importDir.mkdir();
+ if (!success) {
+ Log.e(TAG, "Could not create directory");
+ }
+ }
+ if (success) {
+ txtvPath.setText(importDir.toString());
+ importPath = importDir.toString();
+ } else {
+ txtvPath.setText(R.string.opml_directory_error);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Looks at the contents of the import directory and decides what to do. If
+ * more than one file is in the directory, a dialog will be created to let
+ * the user choose which item to import
+ */
+ private void checkFolderForFiles() {
+ File dir = new File(importPath);
+ if (dir.isDirectory()) {
+ File[] fileList = dir.listFiles();
+ if (fileList.length == 1) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Found one file, choosing that one.");
+ startImport(fileList[0]);
+ } else if (fileList.length > 1) {
+ Log.w(TAG, "Import directory contains more than one file.");
+ askForFile(dir);
+ } else {
+ Log.e(TAG, "Import directory is empty");
+ Toast toast = Toast
+ .makeText(this, R.string.opml_import_error_dir_empty,
+ Toast.LENGTH_LONG);
+ toast.show();
+ }
+ }
+ }
+
+ private void startImport(File file) {
+ Reader mReader = null;
+ try {
+ mReader = new InputStreamReader(new FileInputStream(file),
+ LangUtils.UTF_8);
+ if (BuildConfig.DEBUG) Log.d(TAG, "Parsing " + file.toString());
+ startImport(mReader);
+ } catch (FileNotFoundException e) {
+ Log.d(TAG, "File not found which really should be there");
+ // this should never happen as it is a file we have just chosen
+ }
+ }
+
+ /**
+ * Asks the user to choose from a list of files in a directory and returns
+ * his choice.
+ */
+ private void askForFile(File dir) {
+ final File[] fileList = dir.listFiles();
+ String[] fileNames = dir.list();
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle(R.string.choose_file_to_import_label);
+ dialog.setNeutralButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Dialog was cancelled");
+ dialog.dismiss();
+ }
+ });
+ dialog.setItems(fileNames, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "File at index " + which + " was chosen");
+ dialog.dismiss();
+ startImport(fileList[which]);
+ }
+ });
+ dialog.create().show();
+ }
+
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java
new file mode 100644
index 000000000..7afa270cc
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java
@@ -0,0 +1,29 @@
+package de.danoeh.antennapod.activity;
+
+import de.danoeh.antennapod.core.opml.OpmlElement;
+
+import java.util.ArrayList;
+
+/**
+ * Hold infos gathered by Ompl-Import
+ * <p/>
+ * Created with IntelliJ IDEA.
+ * User: ligi
+ * Date: 1/23/13
+ * Time: 2:15 PM
+ */
+public class OpmlImportHolder {
+
+ private static ArrayList<OpmlElement> readElements;
+
+ public static ArrayList<OpmlElement> getReadElements() {
+ return readElements;
+ }
+
+ public static void setReadElements(ArrayList<OpmlElement> _readElements) {
+ readElements = _readElements;
+ }
+
+
+}
+
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
new file mode 100644
index 000000000..95e352874
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
@@ -0,0 +1,532 @@
+package de.danoeh.antennapod.activity;
+
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources.Theme;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.Build;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.FlattrClickWorker;
+import de.danoeh.antennapod.asynctask.OpmlExportWorker;
+import de.danoeh.antennapod.dialog.AuthenticationDialog;
+import de.danoeh.antennapod.dialog.AutoFlattrPreferenceDialog;
+import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The main preference activity
+ */
+public class PreferenceActivity extends android.preference.PreferenceActivity {
+ private static final String TAG = "PreferenceActivity";
+
+ private static final String PREF_FLATTR_THIS_APP = "prefFlattrThisApp";
+ private static final String PREF_FLATTR_SETTINGS = "prefFlattrSettings";
+ private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate";
+ private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess";
+ private static final String PREF_AUTO_FLATTR_PREFS = "prefAutoFlattrPrefs";
+ private static final String PREF_OPML_EXPORT = "prefOpmlExport";
+ private static final String PREF_ABOUT = "prefAbout";
+ private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir";
+ private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings";
+ private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher";
+
+ private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate";
+ private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
+ private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
+ private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname";
+
+ private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
+ private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify";
+
+ private CheckBoxPreference[] selectedNetworks;
+
+ @SuppressLint("NewApi")
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+
+ if (android.os.Build.VERSION.SDK_INT >= 11) {
+ @SuppressLint("AppCompatMethod") ActionBar ab = getActionBar();
+ if (ab != null) {
+ ab.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ addPreferencesFromResource(R.xml.preferences);
+
+ if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ // disable expanded notification option on unsupported android versions
+ findPreference(PREF_EXPANDED_NOTIFICATION).setEnabled(false);
+ findPreference(PREF_EXPANDED_NOTIFICATION).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Toast toast = Toast.makeText(PreferenceActivity.this, R.string.pref_expand_notify_unsupport_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ return true;
+ }
+ }
+ );
+ }
+
+ findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ new FlattrClickWorker(PreferenceActivity.this,
+ new SimpleFlattrThing(PreferenceActivity.this.getString(R.string.app_name),
+ FlattrUtils.APP_URL,
+ new FlattrStatus(FlattrStatus.STATUS_QUEUE)
+ )
+ ).executeAsync();
+
+ return true;
+ }
+ }
+ );
+
+ findPreference(PREF_FLATTR_REVOKE).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ FlattrUtils.revokeAccessToken(PreferenceActivity.this);
+ checkItemVisibility();
+ return true;
+ }
+
+ }
+ );
+
+ findPreference(PREF_ABOUT).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ PreferenceActivity.this.startActivity(new Intent(
+ PreferenceActivity.this, AboutActivity.class));
+ return true;
+ }
+
+ }
+ );
+
+ findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ new OpmlExportWorker(PreferenceActivity.this)
+ .executeAsync();
+
+ return true;
+ }
+ }
+ );
+
+ findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ startActivityForResult(
+ new Intent(PreferenceActivity.this,
+ DirectoryChooserActivity.class),
+ DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED
+ );
+ return true;
+ }
+ }
+ );
+ findPreference(UserPreferences.PREF_THEME)
+ .setOnPreferenceChangeListener(
+ new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(
+ Preference preference, Object newValue) {
+ Intent i = getIntent();
+ i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ finish();
+ startActivity(i);
+ return true;
+ }
+ }
+ );
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL)
+ .setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue instanceof Boolean) {
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled((Boolean) newValue);
+ setSelectedNetworksEnabled((Boolean) newValue && UserPreferences.isEnableAutodownloadWifiFilter());
+ }
+ return true;
+ }
+ });
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER)
+ .setOnPreferenceChangeListener(
+ new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(
+ Preference preference, Object newValue) {
+ if (newValue instanceof Boolean) {
+ setSelectedNetworksEnabled((Boolean) newValue);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+ );
+ findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE)
+ .setOnPreferenceChangeListener(
+ new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object o) {
+ if (o instanceof String) {
+ setEpisodeCacheSizeText(UserPreferences.readEpisodeCacheSize((String) o));
+ }
+ return true;
+ }
+ }
+ );
+ findPreference(PREF_PLAYBACK_SPEED_LAUNCHER)
+ .setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ VariableSpeedDialog.showDialog(PreferenceActivity.this);
+ return true;
+ }
+ });
+ findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ AuthenticationDialog dialog = new AuthenticationDialog(PreferenceActivity.this,
+ R.string.pref_gpodnet_setlogin_information_title, false, false, GpodnetPreferences.getUsername(),
+ null) {
+
+ @Override
+ protected void onConfirmed(String username, String password, boolean saveUsernamePassword) {
+ GpodnetPreferences.setPassword(password);
+ }
+ };
+ dialog.show();
+ return true;
+ }
+ });
+ findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ GpodnetPreferences.logout();
+ Toast toast = Toast.makeText(PreferenceActivity.this, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ updateGpodnetPreferenceScreen();
+ return true;
+ }
+ });
+ findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ GpodnetSetHostnameDialog.createDialog(PreferenceActivity.this).setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ updateGpodnetPreferenceScreen();
+ }
+ });
+ return true;
+ }
+ });
+
+ findPreference(PREF_AUTO_FLATTR_PREFS).setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ AutoFlattrPreferenceDialog.newAutoFlattrPreferenceDialog(PreferenceActivity.this,
+ new AutoFlattrPreferenceDialog.AutoFlattrPreferenceDialogInterface() {
+ @Override
+ public void onCancelled() {
+
+ }
+
+ @Override
+ public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue) {
+ UserPreferences.setAutoFlattrSettings(PreferenceActivity.this, autoFlattrEnabled, autoFlattrValue);
+ checkItemVisibility();
+ }
+ });
+ return true;
+ }
+ });
+ buildUpdateIntervalPreference();
+ buildAutodownloadSelectedNetworsPreference();
+ setSelectedNetworksEnabled(UserPreferences
+ .isEnableAutodownloadWifiFilter());
+ }
+
+ private void updateGpodnetPreferenceScreen() {
+ final boolean loggedIn = GpodnetPreferences.loggedIn();
+ findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn);
+ findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn);
+ findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn);
+ findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname());
+ }
+
+ private void buildUpdateIntervalPreference() {
+ ListPreference pref = (ListPreference) findPreference(UserPreferences.PREF_UPDATE_INTERVAL);
+ String[] values = getResources().getStringArray(
+ R.array.update_intervall_values);
+ String[] entries = new String[values.length];
+ for (int x = 0; x < values.length; x++) {
+ Integer v = Integer.parseInt(values[x]);
+ switch (v) {
+ case 0:
+ entries[x] = getString(R.string.pref_update_interval_hours_manual);
+ break;
+ case 1:
+ entries[x] = v
+ + " "
+ + getString(R.string.pref_update_interval_hours_singular);
+ break;
+ default:
+ entries[x] = v + " "
+ + getString(R.string.pref_update_interval_hours_plural);
+ break;
+
+ }
+ }
+ pref.setEntries(entries);
+
+ }
+
+ private void setSelectedNetworksEnabled(boolean b) {
+ if (selectedNetworks != null) {
+ for (Preference p : selectedNetworks) {
+ p.setEnabled(b);
+ }
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ checkItemVisibility();
+ setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize());
+ setDataFolderText();
+ updateGpodnetPreferenceScreen();
+ }
+
+ @SuppressWarnings("deprecation")
+ private void checkItemVisibility() {
+
+ boolean hasFlattrToken = FlattrUtils.hasToken();
+
+ findPreference(PREF_FLATTR_SETTINGS).setEnabled(FlattrUtils.hasAPICredentials());
+ findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken);
+ findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken);
+ findPreference(PREF_AUTO_FLATTR_PREFS).setEnabled(hasFlattrToken);
+
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER)
+ .setEnabled(UserPreferences.isEnableAutodownload());
+ setSelectedNetworksEnabled(UserPreferences.isEnableAutodownload()
+ && UserPreferences.isEnableAutodownloadWifiFilter());
+
+ }
+
+ private void setEpisodeCacheSizeText(int cacheSize) {
+ String s;
+ if (cacheSize == getResources().getInteger(
+ R.integer.episode_cache_size_unlimited)) {
+ s = getString(R.string.pref_episode_cache_unlimited);
+ } else {
+ s = Integer.toString(cacheSize)
+ + getString(R.string.episodes_suffix);
+ }
+ findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setSummary(s);
+ }
+
+ private void setDataFolderText() {
+ File f = UserPreferences.getDataFolder(this, null);
+ if (f != null) {
+ findPreference(PREF_CHOOSE_DATA_DIR)
+ .setSummary(f.getAbsolutePath());
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ Intent destIntent = new Intent(this, MainActivity.class);
+ destIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(destIntent);
+ finish();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onApplyThemeResource(Theme theme, int resid, boolean first) {
+ theme.applyStyle(UserPreferences.getTheme(), true);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
+ String dir = data
+ .getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Setting data folder");
+ UserPreferences.setDataFolder(dir);
+ }
+ }
+
+ private void buildAutodownloadSelectedNetworsPreference() {
+ if (selectedNetworks != null) {
+ clearAutodownloadSelectedNetworsPreference();
+ }
+ // get configured networks
+ WifiManager wifiservice = (WifiManager) getSystemService(Context.WIFI_SERVICE);
+ List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks();
+
+ if (networks != null) {
+ selectedNetworks = new CheckBoxPreference[networks.size()];
+ List<String> prefValues = Arrays.asList(UserPreferences
+ .getAutodownloadSelectedNetworks());
+ PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN);
+ OnPreferenceClickListener clickListener = new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference instanceof CheckBoxPreference) {
+ String key = preference.getKey();
+ ArrayList<String> prefValuesList = new ArrayList<String>(
+ Arrays.asList(UserPreferences
+ .getAutodownloadSelectedNetworks())
+ );
+ boolean newValue = ((CheckBoxPreference) preference)
+ .isChecked();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Selected network " + key
+ + ". New state: " + newValue);
+
+ int index = prefValuesList.indexOf(key);
+ if (index >= 0 && newValue == false) {
+ // remove network
+ prefValuesList.remove(index);
+ } else if (index < 0 && newValue == true) {
+ prefValuesList.add(key);
+ }
+
+ UserPreferences.setAutodownloadSelectedNetworks(
+ PreferenceActivity.this, prefValuesList
+ .toArray(new String[prefValuesList
+ .size()])
+ );
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+ // create preference for each known network. attach listener and set
+ // value
+ for (int i = 0; i < networks.size(); i++) {
+ WifiConfiguration config = networks.get(i);
+
+ CheckBoxPreference pref = new CheckBoxPreference(this);
+ String key = Integer.toString(config.networkId);
+ pref.setTitle(config.SSID);
+ pref.setKey(key);
+ pref.setOnPreferenceClickListener(clickListener);
+ pref.setPersistent(false);
+ pref.setChecked(prefValues.contains(key));
+ selectedNetworks[i] = pref;
+ prefScreen.addPreference(pref);
+ }
+ } else {
+ Log.e(TAG, "Couldn't get list of configure Wi-Fi networks");
+ }
+ }
+
+ private void clearAutodownloadSelectedNetworsPreference() {
+ if (selectedNetworks != null) {
+ PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN);
+
+ for (int i = 0; i < selectedNetworks.length; i++) {
+ if (selectedNetworks[i] != null) {
+ prefScreen.removePreference(selectedNetworks[i]);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
+ Preference preference) {
+ super.onPreferenceTreeClick(preferenceScreen, preference);
+ if (preference != null)
+ if (preference instanceof PreferenceScreen)
+ if (((PreferenceScreen) preference).getDialog() != null)
+ ((PreferenceScreen) preference)
+ .getDialog()
+ .getWindow()
+ .getDecorView()
+ .setBackgroundDrawable(
+ this.getWindow().getDecorView()
+ .getBackground().getConstantState()
+ .newDrawable()
+ );
+ return false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ // The default back button behavior has to be overwritten because changing the theme clears the back stack
+ Intent destIntent = new Intent(this, MainActivity.class);
+ destIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(destIntent);
+ finish();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java
new file mode 100644
index 000000000..173bec6b2
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java
@@ -0,0 +1,75 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+
+import org.apache.commons.lang3.StringUtils;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.StorageUtils;
+
+/** Is show if there is now external storage available. */
+public class StorageErrorActivity extends ActionBarActivity {
+ private static final String TAG = "StorageErrorActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.storage_error);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ try {
+ unregisterReceiver(mediaUpdate);
+ } catch (IllegalArgumentException e) {
+
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (StorageUtils.storageAvailable(this)) {
+ leaveErrorState();
+ } else {
+ registerReceiver(mediaUpdate, new IntentFilter(
+ Intent.ACTION_MEDIA_MOUNTED));
+ }
+ }
+
+ private void leaveErrorState() {
+ finish();
+ startActivity(new Intent(this, MainActivity.class));
+ }
+
+ private BroadcastReceiver mediaUpdate = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), Intent.ACTION_MEDIA_MOUNTED)) {
+ if (intent.getBooleanExtra("read-only", true)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Media was mounted; Finishing activity");
+ leaveErrorState();
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Media seemed to have been mounted read only");
+ }
+ }
+ }
+
+ };
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
new file mode 100644
index 000000000..d3df40afb
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
@@ -0,0 +1,359 @@
+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.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.core.util.playback.ExternalMedia;
+import de.danoeh.antennapod.core.util.playback.Playable;
+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);
+ }
+
+ @SuppressLint("AppCompatMethod")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ if (Build.VERSION.SDK_INT >= 11) {
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ }
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ 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 (BuildConfig.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 (BuildConfig.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 (BuildConfig.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 (BuildConfig.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 (BuildConfig.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 (BuildConfig.DEBUG)
+ Log.d(TAG, "Videosurface was destroyed");
+ videoSurfaceCreated = false;
+ controller.notifyVideoSurfaceAbandoned();
+ }
+ };
+
+
+ @Override
+ protected void onReloadNotification(int notificationCode) {
+ if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) {
+ if (BuildConfig.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);
+ }
+
+ @SuppressLint("NewApi")
+ 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);
+ }
+ }
+
+ @SuppressLint("NewApi")
+ 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;
+ }
+
+
+ @Override
+ protected void setScreenOn(boolean enable) {
+ super.setScreenOn(enable);
+ if (enable) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java
new file mode 100644
index 000000000..d7b069b19
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java
@@ -0,0 +1,372 @@
+package de.danoeh.antennapod.activity.gpoddernet;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.NavUtils;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.*;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.GpodnetSyncService;
+
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Guides the user through the authentication process
+ * Step 1: Request username and password from user
+ * Step 2: Choose device from a list of available devices or create a new one
+ * Step 3: Choose from a list of actions
+ */
+public class GpodnetAuthenticationActivity extends ActionBarActivity {
+ private static final String TAG = "GpodnetAuthenticationActivity";
+
+ private static final String CURRENT_STEP = "current_step";
+
+ private ViewFlipper viewFlipper;
+
+ private static final int STEP_DEFAULT = -1;
+ private static final int STEP_LOGIN = 0;
+ private static final int STEP_DEVICE = 1;
+ private static final int STEP_FINISH = 2;
+
+ private int currentStep = -1;
+
+ private GpodnetService service;
+ private volatile String username;
+ private volatile String password;
+ private volatile GpodnetDevice selectedDevice;
+
+ View[] views;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTheme());
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setContentView(R.layout.gpodnetauth_activity);
+ service = new GpodnetService();
+
+ viewFlipper = (ViewFlipper) findViewById(R.id.viewflipper);
+ LayoutInflater inflater = (LayoutInflater)
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ views = new View[]{
+ inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false),
+ inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false),
+ inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false)
+ };
+ for (View view : views) {
+ viewFlipper.addView(view);
+ }
+ advance();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (service != null) {
+ service.shutdown();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ private void setupLoginView(View view) {
+ final EditText username = (EditText) view.findViewById(R.id.etxtUsername);
+ final EditText password = (EditText) view.findViewById(R.id.etxtPassword);
+ final Button login = (Button) view.findViewById(R.id.butLogin);
+ final TextView txtvError = (TextView) view.findViewById(R.id.txtvError);
+ final ProgressBar progressBar = (ProgressBar) view.findViewById(R.id.progBarLogin);
+
+ login.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ final String usernameStr = username.getText().toString();
+ final String passwordStr = password.getText().toString();
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials");
+ new AsyncTask<GpodnetService, Void, Void>() {
+
+ volatile Exception exception;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ login.setEnabled(false);
+ progressBar.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ super.onPostExecute(aVoid);
+ login.setEnabled(true);
+ progressBar.setVisibility(View.GONE);
+
+ if (exception == null) {
+ advance();
+ } else {
+ txtvError.setText(exception.getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected Void doInBackground(GpodnetService... params) {
+ try {
+ params[0].authenticate(usernameStr, passwordStr);
+ GpodnetAuthenticationActivity.this.username = usernameStr;
+ GpodnetAuthenticationActivity.this.password = passwordStr;
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ }
+ return null;
+ }
+ }.execute(service);
+ }
+ });
+ }
+
+ private void setupDeviceView(View view) {
+ final EditText deviceID = (EditText) view.findViewById(R.id.etxtDeviceID);
+ final EditText caption = (EditText) view.findViewById(R.id.etxtCaption);
+ final Button createNewDevice = (Button) view.findViewById(R.id.butCreateNewDevice);
+ final Button chooseDevice = (Button) view.findViewById(R.id.butChooseExistingDevice);
+ final TextView txtvError = (TextView) view.findViewById(R.id.txtvError);
+ final ProgressBar progBarCreateDevice = (ProgressBar) view.findViewById(R.id.progbarCreateDevice);
+ final Spinner spinnerDevices = (Spinner) view.findViewById(R.id.spinnerChooseDevice);
+
+
+ // load device list
+ final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<List<GpodnetDevice>>();
+ new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() {
+
+ private volatile Exception exception;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ chooseDevice.setEnabled(false);
+ spinnerDevices.setEnabled(false);
+ createNewDevice.setEnabled(false);
+ }
+
+ @Override
+ protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) {
+ super.onPostExecute(gpodnetDevices);
+ if (gpodnetDevices != null) {
+ List<String> deviceNames = new ArrayList<String>();
+ for (GpodnetDevice device : gpodnetDevices) {
+ deviceNames.add(device.getCaption());
+ }
+ spinnerDevices.setAdapter(new ArrayAdapter<String>(GpodnetAuthenticationActivity.this,
+ android.R.layout.simple_spinner_dropdown_item, deviceNames));
+ spinnerDevices.setEnabled(true);
+ if (!deviceNames.isEmpty()) {
+ chooseDevice.setEnabled(true);
+ }
+ devices.set(gpodnetDevices);
+ createNewDevice.setEnabled(true);
+ }
+ }
+
+ @Override
+ protected List<GpodnetDevice> doInBackground(GpodnetService... params) {
+ try {
+ return params[0].getDevices(username);
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ }
+ }
+ }.execute(service);
+
+
+ createNewDevice.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (checkDeviceIDText(deviceID, txtvError, devices.get())) {
+ final String deviceStr = deviceID.getText().toString();
+ final String captionStr = caption.getText().toString();
+
+ new AsyncTask<GpodnetService, Void, GpodnetDevice>() {
+
+ private volatile Exception exception;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ createNewDevice.setEnabled(false);
+ chooseDevice.setEnabled(false);
+ progBarCreateDevice.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+ }
+
+ @Override
+ protected void onPostExecute(GpodnetDevice result) {
+ super.onPostExecute(result);
+ createNewDevice.setEnabled(true);
+ chooseDevice.setEnabled(true);
+ progBarCreateDevice.setVisibility(View.GONE);
+ if (exception == null) {
+ selectedDevice = result;
+ advance();
+ } else {
+ txtvError.setText(exception.getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected GpodnetDevice doInBackground(GpodnetService... params) {
+ try {
+ params[0].configureDevice(username, deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE);
+ return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0);
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ }
+ return null;
+ }
+ }.execute(service);
+ }
+ }
+ });
+
+ deviceID.setText(generateDeviceID());
+ chooseDevice.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final int position = spinnerDevices.getSelectedItemPosition();
+ if (position != AdapterView.INVALID_POSITION) {
+ selectedDevice = devices.get().get(position);
+ advance();
+ }
+ }
+ });
+ }
+
+
+ private String generateDeviceID() {
+ final int DEVICE_ID_LENGTH = 10;
+ StringBuilder buffer = new StringBuilder(DEVICE_ID_LENGTH);
+ SecureRandom random = new SecureRandom();
+ for (int i = 0; i < DEVICE_ID_LENGTH; i++) {
+ buffer.append(random.nextInt(10));
+
+ }
+ return buffer.toString();
+ }
+
+ private boolean checkDeviceIDText(EditText deviceID, TextView txtvError, List<GpodnetDevice> devices) {
+ String text = deviceID.getText().toString();
+ if (text.length() == 0) {
+ txtvError.setText(R.string.gpodnetauth_device_errorEmpty);
+ txtvError.setVisibility(View.VISIBLE);
+ return false;
+ } else {
+ if (devices != null) {
+ for (GpodnetDevice device : devices) {
+ if (device.getId().equals(text)) {
+ txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed);
+ txtvError.setVisibility(View.VISIBLE);
+ return false;
+ }
+ }
+ txtvError.setVisibility(View.GONE);
+ return true;
+ }
+ return true;
+ }
+
+ }
+
+ private void setupFinishView(View view) {
+ final Button sync = (Button) view.findViewById(R.id.butSyncNow);
+ final Button back = (Button) view.findViewById(R.id.butGoMainscreen);
+
+ sync.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ GpodnetSyncService.sendSyncIntent(GpodnetAuthenticationActivity.this);
+ finish();
+ }
+ });
+ back.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+ });
+ }
+
+ private void writeLoginCredentials() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Writing login credentials");
+ GpodnetPreferences.setUsername(username);
+ GpodnetPreferences.setPassword(password);
+ GpodnetPreferences.setDeviceID(selectedDevice.getId());
+ }
+
+ private void advance() {
+ if (currentStep < STEP_FINISH) {
+
+ View view = views[currentStep + 1];
+ if (currentStep == STEP_DEFAULT) {
+ setupLoginView(view);
+ } else if (currentStep == STEP_LOGIN) {
+ if (username == null || password == null) {
+ throw new IllegalStateException("Username and password must not be null here");
+ } else {
+ setupDeviceView(view);
+ }
+ } else if (currentStep == STEP_DEVICE) {
+ if (selectedDevice == null) {
+ throw new IllegalStateException("Device must not be null here");
+ } else {
+ writeLoginCredentials();
+ setupFinishView(view);
+ }
+ }
+ if (currentStep != STEP_DEFAULT) {
+ viewFlipper.showNext();
+ }
+ currentStep++;
+ } else {
+ finish();
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java
new file mode 100644
index 000000000..a75789815
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java
@@ -0,0 +1,8 @@
+package de.danoeh.antennapod.adapter;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+public interface ActionButtonCallback {
+ /** Is called when the action button of a list item has been pressed. */
+ abstract void onActionButtonPressed(FeedItem item);
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java
new file mode 100644
index 000000000..fecddeaf4
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java
@@ -0,0 +1,78 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.view.View;
+import android.widget.ImageButton;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+
+/**
+ * Utility methods for the action button that is displayed on the right hand side
+ * of a listitem.
+ */
+public class ActionButtonUtils {
+
+ private final int[] labels;
+ private final TypedArray drawables;
+ private final Context context;
+
+ public ActionButtonUtils(Context context) {
+ Validate.notNull(context);
+
+ this.context = context;
+ drawables = context.obtainStyledAttributes(new int[]{
+ R.attr.av_play, R.attr.navigation_cancel, R.attr.av_download, R.attr.navigation_chapters, R.attr.navigation_accept});
+ labels = new int[]{R.string.play_label, R.string.cancel_download_label, R.string.download_label, R.string.mark_read_label};
+ }
+
+ /**
+ * Sets the displayed bitmap and content description of the given
+ * action button so that it matches the state of the FeedItem.
+ */
+ public void configureActionButton(ImageButton butSecondary, FeedItem item) {
+ Validate.isTrue(butSecondary != null && item != null, "butSecondary or item was null");
+
+ final FeedMedia media = item.getMedia();
+ if (media != null) {
+ final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media);
+ if (!media.isDownloaded()) {
+ if (isDownloadingMedia) {
+ // item is being downloaded
+ butSecondary.setVisibility(View.VISIBLE);
+ butSecondary.setImageDrawable(drawables
+ .getDrawable(1));
+ butSecondary.setContentDescription(context.getString(labels[1]));
+ } else {
+ // item is not downloaded and not being downloaded
+ butSecondary.setVisibility(View.VISIBLE);
+ butSecondary.setImageDrawable(drawables.getDrawable(2));
+ butSecondary.setContentDescription(context.getString(labels[2]));
+ }
+ } else {
+ // item is not being downloaded
+ butSecondary.setVisibility(View.VISIBLE);
+ if (media.isPlaying()) {
+ butSecondary.setImageDrawable(drawables.getDrawable(3));
+ } else {
+ butSecondary
+ .setImageDrawable(drawables.getDrawable(0));
+ }
+ butSecondary.setContentDescription(context.getString(labels[0]));
+ }
+ } else {
+ if (item.isRead()) {
+ butSecondary.setVisibility(View.INVISIBLE);
+ } else {
+ butSecondary.setVisibility(View.VISIBLE);
+ butSecondary.setImageDrawable(drawables.getDrawable(4));
+ butSecondary.setContentDescription(context.getString(labels[3]));
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java
new file mode 100644
index 000000000..c3902639a
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java
@@ -0,0 +1,57 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.res.Resources;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.util.Converter;
+
+/**
+ * Utility methods for adapters
+ */
+public class AdapterUtils {
+
+ private AdapterUtils() {
+
+ }
+
+ /**
+ * Updates the contents of the TextView that shows the current playback position and the ProgressBar.
+ */
+ public static void updateEpisodePlaybackProgress(FeedItem item, Resources res, TextView txtvPos, ProgressBar episodeProgress) {
+ FeedMedia media = item.getMedia();
+ episodeProgress.setVisibility(View.GONE);
+ if (media == null) {
+ txtvPos.setVisibility(View.GONE);
+ return;
+ } else {
+ txtvPos.setVisibility(View.VISIBLE);
+ }
+
+ FeedItem.State state = item.getState();
+ if (state == FeedItem.State.PLAYING
+ || state == FeedItem.State.IN_PROGRESS) {
+ if (media.getDuration() > 0) {
+ episodeProgress.setVisibility(View.VISIBLE);
+ episodeProgress
+ .setProgress((int) (((double) media
+ .getPosition()) / media.getDuration() * 100));
+ txtvPos.setText(Converter
+ .getDurationStringLong(media.getDuration()
+ - media.getPosition()));
+ }
+ } else if (!media.isDownloaded()) {
+ txtvPos.setText(res.getString(
+ R.string.size_prefix)
+ + Converter.byteToString(media.getSize()));
+ } else {
+ txtvPos.setText(res.getString(
+ R.string.length_prefix)
+ + Converter.getDurationStringLong(media
+ .getDuration()));
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java
new file mode 100644
index 000000000..9e59a2a1a
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java
@@ -0,0 +1,180 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.util.ChapterUtils;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+import java.util.List;
+
+public class ChapterListAdapter extends ArrayAdapter<Chapter> {
+
+ private static final String TAG = "ChapterListAdapter";
+
+ private List<Chapter> chapters;
+ private Playable media;
+
+ private int defaultTextColor;
+
+ public ChapterListAdapter(Context context, int textViewResourceId,
+ List<Chapter> objects, Playable media) {
+ super(context, textViewResourceId, objects);
+ this.chapters = objects;
+ this.media = media;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+
+ Chapter sc = getItem(position);
+
+ // Inflate Layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.simplechapter_item, parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ defaultTextColor = holder.title.getTextColors().getDefaultColor();
+ holder.start = (TextView) convertView.findViewById(R.id.txtvStart);
+ holder.link = (TextView) convertView.findViewById(R.id.txtvLink);
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+
+ }
+
+ holder.title.setText(sc.getTitle());
+ holder.start.setText(Converter.getDurationStringLong((int) sc
+ .getStart()));
+ if (sc.getLink() != null) {
+ holder.link.setVisibility(View.VISIBLE);
+ holder.link.setText(sc.getLink());
+ Linkify.addLinks(holder.link, Linkify.WEB_URLS);
+ } else {
+ holder.link.setVisibility(View.GONE);
+ }
+ holder.link.setMovementMethod(null);
+ holder.link.setOnTouchListener(new OnTouchListener() {
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ // from
+ // http://stackoverflow.com/questions/7236840/android-textview-linkify-intercepts-with-parent-view-gestures
+ TextView widget = (TextView) v;
+ Object text = widget.getText();
+ if (text instanceof Spanned) {
+ Spannable buffer = (Spannable) text;
+
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_DOWN) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ ClickableSpan[] link = buffer.getSpans(off, off,
+ ClickableSpan.class);
+
+ if (link.length != 0) {
+ if (action == MotionEvent.ACTION_UP) {
+ link[0].onClick(widget);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ Selection.setSelection(buffer,
+ buffer.getSpanStart(link[0]),
+ buffer.getSpanEnd(link[0]));
+ }
+ return true;
+ }
+ }
+
+ }
+
+ return false;
+
+ }
+ });
+ Chapter current = ChapterUtils.getCurrentChapter(media);
+ if (current != null) {
+ if (current == sc) {
+ holder.title.setTextColor(convertView.getResources().getColor(
+ R.color.bright_blue));
+ holder.start.setTextColor(convertView.getResources().getColor(
+ R.color.bright_blue));
+ } else {
+ holder.title.setTextColor(defaultTextColor);
+ holder.start.setTextColor(defaultTextColor);
+ }
+ } else {
+ Log.w(TAG, "Could not find out what the current chapter is.");
+ }
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView start;
+ TextView link;
+ }
+
+ @Override
+ public int getCount() {
+ // ignore invalid chapters
+ int counter = 0;
+ for (Chapter chapter : chapters) {
+ if (!ignoreChapter(chapter)) {
+ counter++;
+ }
+ }
+ return counter;
+ }
+
+ private boolean ignoreChapter(Chapter c) {
+ return media.getDuration() > 0 && media.getDuration() < c.getStart();
+ }
+
+ @Override
+ public Chapter getItem(int position) {
+ int i = 0;
+ for (Chapter chapter : chapters) {
+ if (!ignoreChapter(chapter)) {
+ if (i == position) {
+ return chapter;
+ } else {
+ i++;
+ }
+ }
+ }
+ return super.getItem(position);
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
new file mode 100644
index 000000000..800462023
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
@@ -0,0 +1,57 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.widget.Toast;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+
+/**
+ * Default implementation of an ActionButtonCallback
+ */
+public class DefaultActionButtonCallback implements ActionButtonCallback {
+ private static final String TAG = "DefaultActionButtonCallback";
+
+ private final Context context;
+
+ public DefaultActionButtonCallback(Context context) {
+ Validate.notNull(context);
+ this.context = context;
+ }
+
+ @Override
+ public void onActionButtonPressed(final FeedItem item) {
+
+
+ if (item.hasMedia()) {
+ final FeedMedia media = item.getMedia();
+ boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media);
+ if (!isDownloading && !media.isDownloaded()) {
+ try {
+ DBTasks.downloadFeedItems(context, item);
+ Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show();
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage());
+ }
+ } else if (isDownloading) {
+ DownloadRequester.getInstance().cancelDownload(context, media);
+ Toast.makeText(context, R.string.download_cancelled_msg, Toast.LENGTH_SHORT).show();
+ } else { // media is downloaded
+ DBTasks.playMedia(context, media, true, true, false);
+ }
+ } else {
+ if (!item.isRead()) {
+ DBWriter.markItemRead(context, item, true, true);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
new file mode 100644
index 000000000..f982e86ce
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
@@ -0,0 +1,112 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedImage;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+
+/** Displays a list of DownloadStatus entries. */
+public class DownloadLogAdapter extends BaseAdapter {
+
+ private Context context;
+
+ private ItemAccess itemAccess;
+
+ public DownloadLogAdapter(Context context, ItemAccess itemAccess) {
+ super();
+ this.itemAccess = itemAccess;
+ this.context = context;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ DownloadStatus status = getItem(position);
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.downloadlog_item, parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.type = (TextView) convertView.findViewById(R.id.txtvType);
+ holder.date = (TextView) convertView.findViewById(R.id.txtvDate);
+ holder.successful = (TextView) convertView
+ .findViewById(R.id.txtvStatus);
+ holder.reason = (TextView) convertView
+ .findViewById(R.id.txtvReason);
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+ if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
+ holder.type.setText(R.string.download_type_feed);
+ } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ holder.type.setText(R.string.download_type_media);
+ } else if (status.getFeedfileType() == FeedImage.FEEDFILETYPE_FEEDIMAGE) {
+ holder.type.setText(R.string.download_type_image);
+ }
+ if (status.getTitle() != null) {
+ holder.title.setText(status.getTitle());
+ } else {
+ holder.title.setText(R.string.download_log_title_unknown);
+ }
+ holder.date.setText(DateUtils.getRelativeTimeSpanString(
+ status.getCompletionDate().getTime(),
+ System.currentTimeMillis(), 0, 0));
+ if (status.isSuccessful()) {
+ holder.successful.setTextColor(convertView.getResources().getColor(
+ R.color.download_success_green));
+ holder.successful.setText(R.string.download_successful);
+ holder.reason.setVisibility(View.GONE);
+ } else {
+ holder.successful.setTextColor(convertView.getResources().getColor(
+ R.color.download_failed_red));
+ holder.successful.setText(R.string.download_failed);
+ String reasonText = status.getReason().getErrorString(context);
+ if (status.getReasonDetailed() != null) {
+ reasonText += ": " + status.getReasonDetailed();
+ }
+ holder.reason.setText(reasonText);
+ holder.reason.setVisibility(View.VISIBLE);
+ }
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView type;
+ TextView date;
+ TextView successful;
+ TextView reason;
+ }
+
+ @Override
+ public int getCount() {
+ return itemAccess.getCount();
+ }
+
+ @Override
+ public DownloadStatus getItem(int position) {
+ return itemAccess.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public static interface ItemAccess {
+ public int getCount();
+ public DownloadStatus getItem(int position);
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java
new file mode 100644
index 000000000..8785916a0
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java
@@ -0,0 +1,122 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.util.Converter;
+
+/**
+ * Shows a list of downloaded episodes
+ */
+public class DownloadedEpisodesListAdapter extends BaseAdapter {
+
+ private final Context context;
+ private final ItemAccess itemAccess;
+
+ private final int imageSize;
+
+ public DownloadedEpisodesListAdapter(Context context, ItemAccess itemAccess) {
+ super();
+ this.context = context;
+ this.itemAccess = itemAccess;
+ this.imageSize = (int) context.getResources().getDimension(R.dimen.thumbnail_length_downloaded_item);
+ }
+
+ @Override
+ public int getCount() {
+ return itemAccess.getCount();
+ }
+
+ @Override
+ public FeedItem getItem(int position) {
+ return itemAccess.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ final FeedItem item = (FeedItem) getItem(position);
+ if (item == null) return null;
+
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.downloaded_episodeslist_item,
+ parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.pubDate = (TextView) convertView
+ .findViewById(R.id.txtvPublished);
+ holder.butSecondary = (ImageButton) convertView
+ .findViewById(R.id.butSecondaryAction);
+ holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage);
+ holder.txtvSize = (TextView) convertView.findViewById(R.id.txtvSize);
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(item.getTitle());
+ holder.pubDate.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_SHOW_DATE));
+ holder.txtvSize.setText(Converter.byteToString(item.getMedia().getSize()));
+ FeedItem.State state = item.getState();
+
+ if (state == FeedItem.State.PLAYING) {
+ holder.butSecondary.setEnabled(false);
+ } else {
+ holder.butSecondary.setEnabled(true);
+ }
+
+ holder.butSecondary.setFocusable(false);
+ holder.butSecondary.setTag(item);
+ holder.butSecondary.setOnClickListener(secondaryActionListener);
+
+
+ PicassoProvider.getMediaMetadataPicassoInstance(context)
+ .load(item.getImageUri())
+ .fit()
+ .into(holder.imageView);
+
+ return convertView;
+ }
+
+ private View.OnClickListener secondaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FeedItem item = (FeedItem) v.getTag();
+ itemAccess.onFeedItemSecondaryAction(item);
+ }
+ };
+
+
+ static class Holder {
+ TextView title;
+ TextView pubDate;
+ ImageView imageView;
+ TextView txtvSize;
+ ImageButton butSecondary;
+ }
+
+ public interface ItemAccess {
+ int getCount();
+
+ FeedItem getItem(int position);
+
+ void onFeedItemSecondaryAction(FeedItem item);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java
new file mode 100644
index 000000000..4257c6eb9
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java
@@ -0,0 +1,142 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.ThemeUtils;
+
+public class DownloadlistAdapter extends BaseAdapter {
+
+ public static final int SELECTION_NONE = -1;
+
+ private int selectedItemIndex;
+ private ItemAccess itemAccess;
+ private Context context;
+
+ public DownloadlistAdapter(Context context,
+ ItemAccess itemAccess) {
+ super();
+ this.selectedItemIndex = SELECTION_NONE;
+ this.context = context;
+ this.itemAccess = itemAccess;
+ }
+
+ @Override
+ public int getCount() {
+ return itemAccess.getCount();
+ }
+
+ @Override
+ public Downloader getItem(int position) {
+ return itemAccess.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ Downloader downloader = getItem(position);
+ DownloadRequest request = downloader.getDownloadRequest();
+ // Inflate layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.downloadlist_item, parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.message = (TextView) convertView
+ .findViewById(R.id.txtvMessage);
+ holder.downloaded = (TextView) convertView
+ .findViewById(R.id.txtvDownloaded);
+ holder.percent = (TextView) convertView
+ .findViewById(R.id.txtvPercent);
+ holder.progbar = (ProgressBar) convertView
+ .findViewById(R.id.progProgress);
+ holder.butSecondary = (ImageButton) convertView
+ .findViewById(R.id.butSecondaryAction);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ if (position == selectedItemIndex) {
+ convertView.setBackgroundColor(convertView.getResources().getColor(
+ ThemeUtils.getSelectionBackgroundColor()));
+ } else {
+ convertView.setBackgroundResource(0);
+ }
+
+ holder.title.setText(request.getTitle());
+ if (request.getStatusMsg() != 0) {
+ holder.message.setText(request.getStatusMsg());
+ }
+ String strDownloaded = Converter.byteToString(request.getSoFar());
+ if (request.getSize() != DownloadStatus.SIZE_UNKNOWN) {
+ strDownloaded += " / " + Converter.byteToString(request.getSize());
+ holder.percent.setText(request.getProgressPercent() + "%");
+ holder.progbar.setProgress(request.getProgressPercent());
+ holder.percent.setVisibility(View.VISIBLE);
+ } else {
+ holder.progbar.setProgress(0);
+ holder.percent.setVisibility(View.INVISIBLE);
+ }
+
+ holder.downloaded.setText(strDownloaded);
+
+ holder.butSecondary.setFocusable(false);
+ holder.butSecondary.setTag(downloader);
+ holder.butSecondary.setOnClickListener(butSecondaryListener);
+
+ return convertView;
+ }
+
+ private View.OnClickListener butSecondaryListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Downloader downloader = (Downloader) v.getTag();
+ itemAccess.onSecondaryActionClick(downloader);
+ }
+ };
+
+ static class Holder {
+ TextView title;
+ TextView message;
+ TextView downloaded;
+ TextView percent;
+ ProgressBar progbar;
+ ImageButton butSecondary;
+ }
+
+ public int getSelectedItemIndex() {
+ return selectedItemIndex;
+ }
+
+ public void setSelectedItemIndex(int selectedItemIndex) {
+ this.selectedItemIndex = selectedItemIndex;
+ notifyDataSetChanged();
+ }
+
+ public interface ItemAccess {
+ public int getCount();
+
+ public Downloader getItem(int position);
+
+ public void onSecondaryActionClick(Downloader downloader);
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
new file mode 100644
index 000000000..8b1ed9112
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
@@ -0,0 +1,306 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.Converter;
+
+/**
+ * Displays unread items and items in the queue in one combined list. The
+ * structure of this list is: [header] [queueItems] [header] [unreadItems].
+ */
+public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter {
+ private static final String TAG = "ExternalEpisodesListAdapter";
+
+ public static final int GROUP_POS_QUEUE = 0;
+ public static final int GROUP_POS_UNREAD = 1;
+
+ private Context context;
+ private ItemAccess itemAccess;
+
+ private ActionButtonCallback feedItemActionCallback;
+ private OnGroupActionClicked groupActionCallback;
+
+ private final int imageSize;
+
+ public ExternalEpisodesListAdapter(Context context,
+ ActionButtonCallback callback,
+ OnGroupActionClicked groupActionCallback,
+ ItemAccess itemAccess) {
+ super();
+ this.context = context;
+ this.itemAccess = itemAccess;
+ this.feedItemActionCallback = callback;
+ this.groupActionCallback = groupActionCallback;
+ this.imageSize = (int) context.getResources().getDimension(R.dimen.thumbnail_length);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public FeedItem getChild(int groupPosition, int childPosition) {
+ if (groupPosition == GROUP_POS_QUEUE) {
+ return itemAccess.getQueueItemAt(childPosition);
+ } else if (groupPosition == GROUP_POS_UNREAD) {
+ return itemAccess.getUnreadItemAt(childPosition);
+ }
+ return null;
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+ Holder holder;
+ final FeedItem item = getChild(groupPosition, childPosition);
+
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.external_itemlist_item,
+ parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.feedTitle = (TextView) convertView
+ .findViewById(R.id.txtvFeedname);
+ holder.lenSize = (TextView) convertView
+ .findViewById(R.id.txtvLenSize);
+ holder.downloadStatus = (ImageView) convertView
+ .findViewById(R.id.imgvDownloadStatus);
+ holder.feedImage = (ImageView) convertView
+ .findViewById(R.id.imgvFeedimage);
+ holder.butAction = (ImageButton) convertView
+ .findViewById(R.id.butAction);
+ holder.statusPlaying = (View) convertView
+ .findViewById(R.id.statusPlaying);
+ holder.episodeProgress = (ProgressBar) convertView
+ .findViewById(R.id.pbar_episode_progress);
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(item.getTitle());
+ holder.feedTitle.setText(item.getFeed().getTitle());
+ FeedItem.State state = item.getState();
+
+ if (groupPosition == GROUP_POS_QUEUE) {
+ switch (state) {
+ case PLAYING:
+ holder.statusPlaying.setVisibility(View.VISIBLE);
+ holder.episodeProgress.setVisibility(View.VISIBLE);
+ break;
+ case IN_PROGRESS:
+ holder.statusPlaying.setVisibility(View.GONE);
+ holder.episodeProgress.setVisibility(View.VISIBLE);
+ break;
+ case NEW:
+ holder.statusPlaying.setVisibility(View.GONE);
+ holder.episodeProgress.setVisibility(View.GONE);
+ break;
+ default:
+ holder.statusPlaying.setVisibility(View.GONE);
+ holder.episodeProgress.setVisibility(View.GONE);
+ break;
+ }
+ } else {
+ holder.statusPlaying.setVisibility(View.GONE);
+ holder.episodeProgress.setVisibility(View.GONE);
+ }
+
+ FeedMedia media = item.getMedia();
+ if (media != null) {
+
+ if (state == FeedItem.State.PLAYING
+ || state == FeedItem.State.IN_PROGRESS) {
+ if (media.getDuration() > 0) {
+ holder.episodeProgress.setProgress((int) (((double) media
+ .getPosition()) / media.getDuration() * 100));
+ holder.lenSize.setText(Converter
+ .getDurationStringLong(media.getDuration()
+ - media.getPosition()));
+ }
+ } else if (!media.isDownloaded()) {
+ holder.lenSize.setText(context.getString(R.string.size_prefix)
+ + Converter.byteToString(media.getSize()));
+ } else {
+ holder.lenSize.setText(context
+ .getString(R.string.length_prefix)
+ + Converter.getDurationStringLong(media.getDuration()));
+ }
+
+ TypedArray drawables = context.obtainStyledAttributes(new int[]{
+ R.attr.av_download, R.attr.navigation_refresh});
+ final int[] labels = new int[]{R.string.status_downloaded_label, R.string.downloading_label};
+ holder.lenSize.setVisibility(View.VISIBLE);
+ if (!media.isDownloaded()) {
+ if (DownloadRequester.getInstance().isDownloadingFile(media)) {
+ holder.downloadStatus.setVisibility(View.VISIBLE);
+ holder.downloadStatus.setImageDrawable(drawables
+ .getDrawable(1));
+ holder.downloadStatus.setContentDescription(context.getString(labels[1]));
+ } else {
+ holder.downloadStatus.setVisibility(View.INVISIBLE);
+ }
+ } else {
+ holder.downloadStatus.setVisibility(View.VISIBLE);
+ holder.downloadStatus
+ .setImageDrawable(drawables.getDrawable(0));
+ holder.downloadStatus.setContentDescription(context.getString(labels[0]));
+ }
+ } else {
+ holder.downloadStatus.setVisibility(View.INVISIBLE);
+ holder.lenSize.setVisibility(View.INVISIBLE);
+ }
+
+ PicassoProvider.getMediaMetadataPicassoInstance(context)
+ .load(item.getImageUri())
+ .fit()
+ .into(holder.feedImage);
+
+ holder.butAction.setFocusable(false);
+ holder.butAction.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ feedItemActionCallback.onActionButtonPressed(item);
+ }
+ });
+
+ return convertView;
+
+ }
+
+ static class Holder {
+ TextView title;
+ TextView feedTitle;
+ TextView lenSize;
+ ImageView downloadStatus;
+ ImageView feedImage;
+ ImageButton butAction;
+ View statusPlaying;
+ ProgressBar episodeProgress;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ if (groupPosition == GROUP_POS_QUEUE) {
+ return itemAccess.getQueueSize();
+ } else if (groupPosition == GROUP_POS_UNREAD) {
+ return itemAccess.getUnreadItemsSize();
+ }
+ return 0;
+ }
+
+ @Override
+ public int getGroupCount() {
+ // Hide 'unread items' group if empty
+ if (itemAccess.getUnreadItemsSize() > 0) {
+ return 2;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ @Override
+ public View getGroupView(final int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.feeditemlist_header, parent, false);
+ TextView headerTitle = (TextView) convertView
+ .findViewById(0);
+ ImageButton actionButton = (ImageButton) convertView
+ .findViewById(R.id.butAction);
+ TextView numItems = (TextView) convertView.findViewById(0);
+
+ String headerString = null;
+ int childrenCount = 0;
+
+ if (groupPosition == 0) {
+ headerString = context.getString(R.string.queue_label);
+ childrenCount = getChildrenCount(GROUP_POS_QUEUE);
+ } else {
+ headerString = context.getString(R.string.waiting_list_label);
+ childrenCount = getChildrenCount(GROUP_POS_UNREAD);
+ }
+ headerTitle.setText(headerString);
+ if (childrenCount <= 0) {
+ numItems.setVisibility(View.INVISIBLE);
+ } else {
+ numItems.setVisibility(View.VISIBLE);
+ numItems.setText(Integer.toString(childrenCount));
+ }
+ actionButton.setFocusable(false);
+ actionButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ groupActionCallback.onClick(getGroupId(groupPosition));
+ }
+ });
+ return convertView;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return itemAccess.getUnreadItemsSize() == 0
+ && itemAccess.getQueueSize() == 0;
+ }
+
+ @Override
+ public Object getGroup(int groupPosition) {
+ return null;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public interface OnGroupActionClicked {
+ public void onClick(long groupId);
+ }
+
+ public static interface ItemAccess {
+ public int getQueueSize();
+
+ public int getUnreadItemsSize();
+
+ public FeedItem getQueueItemAt(int position);
+
+ public FeedItem getUnreadItemAt(int position);
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
new file mode 100644
index 000000000..2f69e6580
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
@@ -0,0 +1,219 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.*;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.ThemeUtils;
+
+/**
+ * List adapter for items of feeds that the user has already subscribed to.
+ */
+public class FeedItemlistAdapter extends BaseAdapter {
+
+ private ActionButtonCallback callback;
+ private final ItemAccess itemAccess;
+ private final Context context;
+ private boolean showFeedtitle;
+ private int selectedItemIndex;
+ private final ActionButtonUtils actionButtonUtils;
+
+ public static final int SELECTION_NONE = -1;
+
+ public FeedItemlistAdapter(Context context,
+ ItemAccess itemAccess,
+ ActionButtonCallback callback, boolean showFeedtitle) {
+ super();
+ this.callback = callback;
+ this.context = context;
+ this.itemAccess = itemAccess;
+ this.showFeedtitle = showFeedtitle;
+ this.selectedItemIndex = SELECTION_NONE;
+ this.actionButtonUtils = new ActionButtonUtils(context);
+ }
+
+ @Override
+ public int getCount() {
+ return itemAccess.getCount();
+
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public FeedItem getItem(int position) {
+ return itemAccess.getItem(position);
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ final FeedItem item = getItem(position);
+
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.feeditemlist_item, parent, false);
+ holder.title = (TextView) convertView
+ .findViewById(R.id.txtvItemname);
+ holder.lenSize = (TextView) convertView
+ .findViewById(R.id.txtvLenSize);
+ holder.butAction = (ImageButton) convertView
+ .findViewById(R.id.butSecondaryAction);
+ holder.published = (TextView) convertView
+ .findViewById(R.id.txtvPublished);
+ holder.inPlaylist = (ImageView) convertView
+ .findViewById(R.id.imgvInPlaylist);
+ holder.type = (ImageView) convertView.findViewById(R.id.imgvType);
+ holder.statusUnread = (View) convertView
+ .findViewById(R.id.statusUnread);
+ holder.episodeProgress = (ProgressBar) convertView
+ .findViewById(R.id.pbar_episode_progress);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+ if (!(getItemViewType(position) == Adapter.IGNORE_ITEM_VIEW_TYPE)) {
+ convertView.setVisibility(View.VISIBLE);
+ if (position == selectedItemIndex) {
+ convertView.setBackgroundColor(convertView.getResources()
+ .getColor(ThemeUtils.getSelectionBackgroundColor()));
+ } else {
+ convertView.setBackgroundResource(0);
+ }
+
+ StringBuilder buffer = new StringBuilder(item.getTitle());
+ if (showFeedtitle) {
+ buffer.append("(");
+ buffer.append(item.getFeed().getTitle());
+ buffer.append(")");
+ }
+ holder.title.setText(buffer.toString());
+
+ FeedItem.State state = item.getState();
+ switch (state) {
+ case PLAYING:
+ holder.statusUnread.setVisibility(View.GONE);
+ holder.episodeProgress.setVisibility(View.VISIBLE);
+ break;
+ case IN_PROGRESS:
+ holder.statusUnread.setVisibility(View.GONE);
+ holder.episodeProgress.setVisibility(View.VISIBLE);
+ break;
+ case NEW:
+ holder.statusUnread.setVisibility(View.VISIBLE);
+ break;
+ default:
+ holder.statusUnread.setVisibility(View.GONE);
+ break;
+ }
+
+ holder.published.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_SHOW_DATE));
+
+
+ FeedMedia media = item.getMedia();
+ if (media == null) {
+ holder.episodeProgress.setVisibility(View.GONE);
+ holder.inPlaylist.setVisibility(View.INVISIBLE);
+ holder.type.setVisibility(View.INVISIBLE);
+ holder.lenSize.setVisibility(View.INVISIBLE);
+ } else {
+
+ AdapterUtils.updateEpisodePlaybackProgress(item, context.getResources(), holder.lenSize, holder.episodeProgress);
+
+ if (((ItemAccess) itemAccess).isInQueue(item)) {
+ holder.inPlaylist.setVisibility(View.VISIBLE);
+ } else {
+ holder.inPlaylist.setVisibility(View.INVISIBLE);
+ }
+
+ if (DownloadRequester.getInstance().isDownloadingFile(
+ item.getMedia())) {
+ holder.episodeProgress.setVisibility(View.VISIBLE);
+ holder.episodeProgress.setProgress(((ItemAccess) itemAccess).getItemDownloadProgressPercent(item));
+ }
+
+ TypedArray typeDrawables = context.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(context.getString(labels[0]));
+ holder.type.setVisibility(View.VISIBLE);
+ } else if (mediaType == MediaType.VIDEO) {
+ holder.type.setImageDrawable(typeDrawables.getDrawable(1));
+ holder.type.setContentDescription(context.getString(labels[1]));
+ holder.type.setVisibility(View.VISIBLE);
+ } else {
+ holder.type.setImageBitmap(null);
+ holder.type.setVisibility(View.GONE);
+ }
+ }
+
+ actionButtonUtils.configureActionButton(holder.butAction, item);
+ holder.butAction.setFocusable(false);
+ holder.butAction.setTag(item);
+ holder.butAction.setOnClickListener(butActionListener);
+
+ } else {
+ convertView.setVisibility(View.GONE);
+ }
+ return convertView;
+
+ }
+
+ private final OnClickListener butActionListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FeedItem item = (FeedItem) v.getTag();
+ callback.onActionButtonPressed(item);
+ }
+ };
+
+ static class Holder {
+ TextView title;
+ TextView published;
+ TextView lenSize;
+ ImageView type;
+ ImageView inPlaylist;
+ ImageButton butAction;
+ View statusUnread;
+ ProgressBar episodeProgress;
+ }
+
+ public int getSelectedItemIndex() {
+ return selectedItemIndex;
+ }
+
+ public void setSelectedItemIndex(int selectedItemIndex) {
+ this.selectedItemIndex = selectedItemIndex;
+ notifyDataSetChanged();
+ }
+
+ public static interface ItemAccess {
+ public boolean isInQueue(FeedItem item);
+
+ int getItemDownloadProgressPercent(FeedItem item);
+
+ int getCount();
+
+ FeedItem getItem(int position);
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
new file mode 100644
index 000000000..9011c8b02
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+import java.util.List;
+
+/**
+ * List adapter for showing a list of FeedItems with their title and description.
+ */
+public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
+
+ public FeedItemlistDescriptionAdapter(Context context, int resource, List<FeedItem> objects) {
+ super(context, resource, objects);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+
+ FeedItem item = getItem(position);
+
+ // Inflate layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.description = (TextView) convertView.findViewById(R.id.txtvDescription);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(item.getTitle());
+ if (item.getDescription() != null) {
+ holder.description.setText(item.getDescription());
+ }
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView description;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
new file mode 100644
index 000000000..a917633e6
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
@@ -0,0 +1,229 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.feed.Feed;
+
+/**
+ * BaseAdapter for the navigation drawer
+ */
+public class NavListAdapter extends BaseAdapter {
+ public static final int VIEW_TYPE_COUNT = 3;
+ public static final int VIEW_TYPE_NAV = 0;
+ public static final int VIEW_TYPE_SECTION_DIVIDER = 1;
+ public static final int VIEW_TYPE_SUBSCRIPTION = 2;
+
+ public static final int[] NAV_TITLES = {R.string.all_episodes_label, R.string.queue_label, R.string.downloads_label, R.string.playback_history_label, R.string.add_feed_label};
+
+ private final Drawable[] drawables;
+
+ public static final int SUBSCRIPTION_OFFSET = 1 + NAV_TITLES.length;
+
+ private ItemAccess itemAccess;
+ private Context context;
+
+ public NavListAdapter(ItemAccess itemAccess, Context context) {
+ this.itemAccess = itemAccess;
+ this.context = context;
+
+ TypedArray ta = context.obtainStyledAttributes(new int[]{R.attr.ic_new, R.attr.stat_playlist,
+ R.attr.av_download, R.attr.device_access_time, R.attr.content_new});
+ drawables = new Drawable[]{ta.getDrawable(0), ta.getDrawable(1), ta.getDrawable(2),
+ ta.getDrawable(3), ta.getDrawable(4)};
+ ta.recycle();
+ }
+
+ @Override
+ public int getCount() {
+ return NAV_TITLES.length + 1 + itemAccess.getCount();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ int viewType = getItemViewType(position);
+ if (viewType == VIEW_TYPE_NAV) {
+ return context.getString(NAV_TITLES[position]);
+ } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) {
+ return context.getString(R.string.podcasts_label);
+ } else {
+ return itemAccess.getItem(position);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (0 <= position && position < NAV_TITLES.length) {
+ return VIEW_TYPE_NAV;
+ } else if (position < NAV_TITLES.length + 1) {
+ return VIEW_TYPE_SECTION_DIVIDER;
+ } else {
+ return VIEW_TYPE_SUBSCRIPTION;
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ int viewType = getItemViewType(position);
+ View v = null;
+ if (viewType == VIEW_TYPE_NAV) {
+ v = getNavView((String) getItem(position), position, convertView, parent);
+ } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) {
+ v = getSectionDividerView((String) getItem(position), position, convertView, parent);
+ } else {
+ v = getFeedView(position - SUBSCRIPTION_OFFSET, convertView, parent);
+ }
+ if (v != null) {
+ TextView txtvTitle = (TextView) v.findViewById(R.id.txtvTitle);
+ if (position == itemAccess.getSelectedItemIndex()) {
+ txtvTitle.setTypeface(null, Typeface.BOLD);
+ } else {
+ txtvTitle.setTypeface(null, Typeface.NORMAL);
+ }
+ }
+ return v;
+ }
+
+ private View getNavView(String title, int position, View convertView, ViewGroup parent) {
+ NavHolder holder;
+ if (convertView == null) {
+ holder = new NavHolder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.nav_listitem, parent, false);
+
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.count = (TextView) convertView.findViewById(R.id.txtvCount);
+ holder.image = (ImageView) convertView.findViewById(R.id.imgvCover);
+ convertView.setTag(holder);
+ } else {
+ holder = (NavHolder) convertView.getTag();
+ }
+
+ holder.title.setText(title);
+
+ if (NAV_TITLES[position] == R.string.queue_label) {
+ int queueSize = itemAccess.getQueueSize();
+ if (queueSize > 0) {
+ holder.count.setVisibility(View.VISIBLE);
+ holder.count.setText(String.valueOf(queueSize));
+ } else {
+ holder.count.setVisibility(View.GONE);
+ }
+ } else if (NAV_TITLES[position] == R.string.all_episodes_label) {
+ int unreadItems = itemAccess.getNumberOfUnreadItems();
+ if (unreadItems > 0) {
+ holder.count.setVisibility(View.VISIBLE);
+ holder.count.setText(String.valueOf(unreadItems));
+ } else {
+ holder.count.setVisibility(View.GONE);
+ }
+ } else {
+ holder.count.setVisibility(View.GONE);
+ }
+
+ holder.image.setImageDrawable(drawables[position]);
+
+ return convertView;
+ }
+
+ private View getSectionDividerView(String title, int position, View convertView, ViewGroup parent) {
+ SectionHolder holder;
+ if (convertView == null) {
+ holder = new SectionHolder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.nav_section_item, parent, false);
+
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ convertView.setTag(holder);
+ } else {
+ holder = (SectionHolder) convertView.getTag();
+ }
+
+ holder.title.setText(title);
+
+ convertView.setEnabled(false);
+ convertView.setOnClickListener(null);
+
+ return convertView;
+ }
+
+ private View getFeedView(int feedPos, View convertView, ViewGroup parent) {
+ FeedHolder holder;
+ Feed feed = itemAccess.getItem(feedPos);
+
+ if (convertView == null) {
+ holder = new FeedHolder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.nav_feedlistitem, parent, false);
+
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.image = (ImageView) convertView.findViewById(R.id.imgvCover);
+ convertView.setTag(holder);
+ } else {
+ holder = (FeedHolder) convertView.getTag();
+ }
+
+ holder.title.setText(feed.getTitle());
+
+ PicassoProvider.getDefaultPicassoInstance(context)
+ .load(feed.getImageUri())
+ .fit()
+ .into(holder.image);
+
+ return convertView;
+ }
+
+ static class NavHolder {
+ TextView title;
+ TextView count;
+ ImageView image;
+ }
+
+ static class SectionHolder {
+ TextView title;
+ }
+
+ static class FeedHolder {
+ TextView title;
+ ImageView image;
+ }
+
+
+ public interface ItemAccess {
+ public int getCount();
+
+ public Feed getItem(int position);
+
+ public int getSelectedItemIndex();
+
+ public int getQueueSize();
+
+ public int getNumberOfUnreadItems();
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java
new file mode 100644
index 000000000..a0829286c
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java
@@ -0,0 +1,170 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.Converter;
+
+/**
+ * List adapter for the list of new episodes
+ */
+public class NewEpisodesListAdapter extends BaseAdapter {
+
+ private final Context context;
+ private final ItemAccess itemAccess;
+ private final ActionButtonCallback actionButtonCallback;
+ private final ActionButtonUtils actionButtonUtils;
+
+ public NewEpisodesListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback) {
+ super();
+ this.context = context;
+ this.itemAccess = itemAccess;
+ this.actionButtonUtils = new ActionButtonUtils(context);
+ this.actionButtonCallback = actionButtonCallback;
+ }
+
+ @Override
+ public int getCount() {
+ return itemAccess.getCount();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return itemAccess.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ final FeedItem item = (FeedItem) getItem(position);
+ if (item == null) return null;
+
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.new_episodes_listitem,
+ parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.pubDate = (TextView) convertView
+ .findViewById(R.id.txtvPublished);
+ holder.statusUnread = convertView.findViewById(R.id.statusUnread);
+ holder.butSecondary = (ImageButton) convertView
+ .findViewById(R.id.butSecondaryAction);
+ holder.queueStatus = (ImageView) convertView
+ .findViewById(R.id.imgvInPlaylist);
+ holder.downloadProgress = (ProgressBar) convertView
+ .findViewById(R.id.pbar_download_progress);
+ holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage);
+ holder.txtvDuration = (TextView) convertView.findViewById(R.id.txtvDuration);
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(item.getTitle());
+ holder.pubDate.setText(DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_SHOW_DATE));
+ if (item.isRead()) {
+ holder.statusUnread.setVisibility(View.GONE);
+ } else {
+ holder.statusUnread.setVisibility(View.VISIBLE);
+ }
+
+ FeedMedia media = item.getMedia();
+ if (media != null) {
+ final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media);
+
+ if (media.getDuration() > 0) {
+ holder.txtvDuration.setText(Converter.getDurationStringLong(media.getDuration()));
+ } else {
+ holder.txtvDuration.setText("");
+ }
+
+ if (isDownloadingMedia) {
+ holder.downloadProgress.setVisibility(View.VISIBLE);
+ holder.txtvDuration.setVisibility(View.GONE);
+ } else {
+ holder.txtvDuration.setVisibility(View.VISIBLE);
+ holder.downloadProgress.setVisibility(View.GONE);
+ }
+
+ if (!media.isDownloaded()) {
+ if (isDownloadingMedia) {
+ // item is being downloaded
+ holder.downloadProgress.setProgress(itemAccess.getItemDownloadProgressPercent(item));
+ }
+ }
+ }
+ if (itemAccess.isInQueue(item)) {
+ holder.queueStatus.setVisibility(View.VISIBLE);
+ } else {
+ holder.queueStatus.setVisibility(View.INVISIBLE);
+ }
+
+ actionButtonUtils.configureActionButton(holder.butSecondary, item);
+ holder.butSecondary.setFocusable(false);
+ holder.butSecondary.setTag(item);
+ holder.butSecondary.setOnClickListener(secondaryActionListener);
+
+ PicassoProvider.getMediaMetadataPicassoInstance(context)
+ .load(item.getImageUri())
+ .fit()
+ .into(holder.imageView);
+
+ return convertView;
+ }
+
+ private View.OnClickListener secondaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FeedItem item = (FeedItem) v.getTag();
+ actionButtonCallback.onActionButtonPressed(item);
+ }
+ };
+
+
+ static class Holder {
+ TextView title;
+ TextView pubDate;
+ View statusUnread;
+ ImageView queueStatus;
+ ImageView imageView;
+ ProgressBar downloadProgress;
+ TextView txtvDuration;
+ ImageButton butSecondary;
+ }
+
+ public interface ItemAccess {
+
+ int getCount();
+
+ FeedItem getItem(int position);
+
+ int getItemDownloadProgressPercent(FeedItem item);
+
+ boolean isInQueue(FeedItem item);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java
new file mode 100644
index 000000000..bc42ad063
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java
@@ -0,0 +1,127 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+
+/**
+ * List adapter for the queue.
+ */
+public class QueueListAdapter extends BaseAdapter {
+
+
+ private final Context context;
+ private final ItemAccess itemAccess;
+ private final ActionButtonCallback actionButtonCallback;
+ private final ActionButtonUtils actionButtonUtils;
+
+
+ public QueueListAdapter(Context context, ItemAccess itemAccess, ActionButtonCallback actionButtonCallback) {
+ super();
+ this.context = context;
+ this.itemAccess = itemAccess;
+ this.actionButtonUtils = new ActionButtonUtils(context);
+ this.actionButtonCallback = actionButtonCallback;
+ }
+
+ @Override
+ public int getCount() {
+ return itemAccess.getCount();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return itemAccess.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ final FeedItem item = (FeedItem) getItem(position);
+ if (item == null) return null;
+
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.queue_listitem,
+ parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.butSecondary = (ImageButton) convertView
+ .findViewById(R.id.butSecondaryAction);
+ holder.position = (TextView) convertView.findViewById(R.id.txtvPosition);
+ holder.progress = (ProgressBar) convertView
+ .findViewById(R.id.pbar_download_progress);
+ holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage);
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(item.getTitle());
+
+ AdapterUtils.updateEpisodePlaybackProgress(item, context.getResources(), holder.position, holder.progress);
+
+ FeedMedia media = item.getMedia();
+ if (media != null) {
+ final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media);
+
+ if (!media.isDownloaded()) {
+ if (isDownloadingMedia) {
+ // item is being downloaded
+ holder.progress.setVisibility(View.VISIBLE);
+ holder.progress.setProgress(itemAccess.getItemDownloadProgressPercent(item));
+ }
+ }
+ }
+
+ actionButtonUtils.configureActionButton(holder.butSecondary, item);
+ holder.butSecondary.setFocusable(false);
+ holder.butSecondary.setTag(item);
+ holder.butSecondary.setOnClickListener(secondaryActionListener);
+
+ PicassoProvider.getMediaMetadataPicassoInstance(context)
+ .load(item.getImageUri())
+ .fit()
+ .into(holder.imageView);
+
+ return convertView;
+ }
+
+ private View.OnClickListener secondaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FeedItem item = (FeedItem) v.getTag();
+ actionButtonCallback.onActionButtonPressed(item);
+ }
+ };
+
+
+ static class Holder {
+ TextView title;
+ ImageView imageView;
+ TextView position;
+ ProgressBar progress;
+ ImageButton butSecondary;
+ }
+
+ public interface ItemAccess {
+ int getCount();
+
+ FeedItem getItem(int position);
+
+ int getItemDownloadProgressPercent(FeedItem item);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java
new file mode 100644
index 000000000..79c1f6f99
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java
@@ -0,0 +1,110 @@
+package de.danoeh.antennapod.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedComponent;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.SearchResult;
+
+/**
+ * List adapter for search activity.
+ */
+public class SearchlistAdapter extends BaseAdapter {
+
+ private final Context context;
+ private final ItemAccess itemAccess;
+
+
+ public SearchlistAdapter(Context context, ItemAccess itemAccess) {
+ this.context = context;
+ this.itemAccess = itemAccess;
+ }
+
+ @Override
+ public int getCount() {
+ return itemAccess.getCount();
+ }
+
+ @Override
+ public SearchResult getItem(int position) {
+ return itemAccess.getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final Holder holder;
+ SearchResult result = getItem(position);
+ FeedComponent component = result.getComponent();
+
+ // Inflate Layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.searchlist_item, parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.cover = (ImageView) convertView
+ .findViewById(R.id.imgvFeedimage);
+ holder.subtitle = (TextView) convertView
+ .findViewById(R.id.txtvSubtitle);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+ if (component.getClass() == Feed.class) {
+ final Feed feed = (Feed) component;
+ holder.title.setText(feed.getTitle());
+ holder.subtitle.setVisibility(View.GONE);
+
+ PicassoProvider.getDefaultPicassoInstance(context)
+ .load(feed.getImageUri())
+ .fit()
+ .into(holder.cover);
+
+ } else if (component.getClass() == FeedItem.class) {
+ final FeedItem item = (FeedItem) component;
+ holder.title.setText(item.getTitle());
+ if (result.getSubtitle() != null) {
+ holder.subtitle.setVisibility(View.VISIBLE);
+ holder.subtitle.setText(result.getSubtitle());
+ }
+
+ PicassoProvider.getDefaultPicassoInstance(context)
+ .load(item.getFeed().getImageUri())
+ .fit()
+ .into(holder.cover);
+
+ }
+
+ return convertView;
+ }
+
+ static class Holder {
+ ImageView cover;
+ TextView title;
+ TextView subtitle;
+ }
+
+ public static interface ItemAccess {
+ int getCount();
+
+ SearchResult getItem(int position);
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
new file mode 100644
index 000000000..7bee8e861
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
@@ -0,0 +1,68 @@
+package de.danoeh.antennapod.adapter.gpodnet;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
+
+/**
+ * Adapter for displaying a list of GPodnetPodcast-Objects.
+ */
+public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> {
+
+ public PodcastListAdapter(Context context, int resource, List<GpodnetPodcast> objects) {
+ super(context, resource, objects);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+
+ GpodnetPodcast podcast = getItem(position);
+
+ // Inflate Layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, parent, false);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.description = (TextView) convertView.findViewById(R.id.txtvDescription);
+ holder.image = (ImageView) convertView.findViewById(R.id.imgvCover);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ holder.title.setText(podcast.getTitle());
+ holder.description.setText(podcast.getDescription());
+
+ if (StringUtils.isNoneBlank(podcast.getLogoUrl())) {
+ PicassoProvider.getDefaultPicassoInstance(convertView.getContext())
+ .load(podcast.getLogoUrl())
+ .fit()
+ .into(holder.image);
+ }
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView description;
+ ImageView image;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
new file mode 100644
index 000000000..6bba956a6
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
@@ -0,0 +1,118 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.opml.OpmlWriter;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.LangUtils;
+
+/**
+ * Writes an OPML file into the export directory in the background.
+ */
+public class OpmlExportWorker extends AsyncTask<Void, Void, Void> {
+ private static final String TAG = "OpmlExportWorker";
+ private static final String DEFAULT_OUTPUT_NAME = "antennapod-feeds.opml";
+ public static final String EXPORT_DIR = "export/";
+
+ private Context context;
+ private File output;
+
+ private ProgressDialog progDialog;
+ private Exception exception;
+
+ public OpmlExportWorker(Context context, File output) {
+ this.context = context;
+ this.output = output;
+ }
+
+ public OpmlExportWorker(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ OpmlWriter opmlWriter = new OpmlWriter();
+ if (output == null) {
+ output = new File(
+ UserPreferences.getDataFolder(context, EXPORT_DIR),
+ DEFAULT_OUTPUT_NAME);
+ if (output.exists()) {
+ Log.w(TAG, "Overwriting previously exported file.");
+ output.delete();
+ }
+ }
+ OutputStreamWriter writer = null;
+ try {
+ writer = new OutputStreamWriter(new FileOutputStream(output), LangUtils.UTF_8);
+ opmlWriter.writeDocument(DBReader.getFeedList(context), writer);
+ } catch (IOException e) {
+ e.printStackTrace();
+ exception = e;
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException ioe) {
+ exception = ioe;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ progDialog.dismiss();
+ AlertDialog.Builder alert = new AlertDialog.Builder(context)
+ .setNeutralButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ dialog.dismiss();
+ }
+ });
+ if (exception != null) {
+ alert.setTitle(R.string.export_error_label);
+ alert.setMessage(exception.getMessage());
+ } else {
+ alert.setTitle(R.string.opml_export_success_title);
+ alert.setMessage(context
+ .getString(R.string.opml_export_success_sum)
+ + output.toString());
+ }
+ alert.create().show();
+ }
+
+ @Override
+ protected void onPreExecute() {
+ progDialog = new ProgressDialog(context);
+ progDialog.setMessage(context.getString(R.string.exporting_label));
+ progDialog.setIndeterminate(true);
+ progDialog.show();
+ }
+
+ @SuppressLint("NewApi")
+ public void executeAsync() {
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ executeOnExecutor(THREAD_POOL_EXECUTOR);
+ } else {
+ execute();
+ }
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
new file mode 100644
index 000000000..cb9197b8e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.annotation.SuppressLint;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.os.AsyncTask;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.activity.OpmlImportHolder;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.opml.OpmlElement;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+
+import java.util.Arrays;
+import java.util.Date;
+
+/** Queues items for download in the background. */
+public class OpmlFeedQueuer extends AsyncTask<Void, Void, Void> {
+ private Context context;
+ private ProgressDialog progDialog;
+ private int[] selection;
+
+ public OpmlFeedQueuer(Context context, int[] selection) {
+ super();
+ this.context = context;
+ this.selection = Arrays.copyOf(selection, selection.length);
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ progDialog.dismiss();
+ }
+
+ @Override
+ protected void onPreExecute() {
+ progDialog = new ProgressDialog(context);
+ progDialog.setMessage(context.getString(R.string.processing_label));
+ progDialog.setCancelable(false);
+ progDialog.setIndeterminate(true);
+ progDialog.show();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ DownloadRequester requester = DownloadRequester.getInstance();
+ for (int idx = 0; idx < selection.length; idx++) {
+ OpmlElement element = OpmlImportHolder.getReadElements().get(
+ selection[idx]);
+ Feed feed = new Feed(element.getXmlUrl(), new Date(),
+ element.getText());
+ try {
+ requester.downloadFeed(context.getApplicationContext(), feed);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ @SuppressLint("NewApi")
+ public void executeAsync() {
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ executeOnExecutor(THREAD_POOL_EXECUTOR);
+ } else {
+ execute();
+ }
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
new file mode 100644
index 000000000..cfe0703ca
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
@@ -0,0 +1,116 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.opml.OpmlElement;
+import de.danoeh.antennapod.core.opml.OpmlReader;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+
+public class OpmlImportWorker extends
+ AsyncTask<Void, Void, ArrayList<OpmlElement>> {
+ private static final String TAG = "OpmlImportWorker";
+
+ private Context context;
+ private Exception exception;
+
+ private ProgressDialog progDialog;
+
+ private Reader mReader;
+
+ public OpmlImportWorker(Context context, Reader reader) {
+ super();
+ this.context = context;
+ this.mReader=reader;
+ }
+
+ @Override
+ protected ArrayList<OpmlElement> doInBackground(Void... params) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting background work");
+
+ if (mReader==null) {
+ return null;
+ }
+
+ OpmlReader opmlReader = new OpmlReader();
+ try {
+ ArrayList<OpmlElement> result = opmlReader.readDocument(mReader);
+ mReader.close();
+ return result;
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ }
+
+ }
+
+ @Override
+ protected void onPostExecute(ArrayList<OpmlElement> result) {
+ if (mReader != null) {
+ try {
+ mReader.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ progDialog.dismiss();
+ if (exception != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "An error occured while trying to parse the opml document");
+ AlertDialog.Builder alert = new AlertDialog.Builder(context);
+ alert.setTitle(R.string.error_label);
+ alert.setMessage(context.getString(R.string.opml_reader_error)
+ + exception.getMessage());
+ alert.setNeutralButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+
+ });
+ alert.create().show();
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ progDialog = new ProgressDialog(context);
+ progDialog.setMessage(context.getString(R.string.reading_opml_label));
+ progDialog.setIndeterminate(true);
+ progDialog.setCancelable(false);
+ progDialog.show();
+ }
+
+ public boolean wasSuccessful() {
+ return exception != null;
+ }
+
+ @SuppressLint("NewApi")
+ public void executeAsync() {
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ executeOnExecutor(THREAD_POOL_EXECUTOR);
+ } else {
+ execute();
+ }
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java
new file mode 100644
index 000000000..4d9be5d78
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java
@@ -0,0 +1,29 @@
+package de.danoeh.antennapod.config;
+
+
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.activity.StorageErrorActivity;
+import de.danoeh.antennapod.core.ApplicationCallbacks;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+public class ApplicationCallbacksImpl implements ApplicationCallbacks {
+
+ @Override
+ public Application getApplicationInstance() {
+ return PodcastApp.getInstance();
+ }
+
+ @Override
+ public Intent getStorageErrorActivity(Context context) {
+ return new Intent(context, StorageErrorActivity.class);
+ }
+
+ @Override
+ public void setUpdateInterval(long updateInterval) {
+ UserPreferences.restartUpdateAlarm(updateInterval, updateInterval);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
new file mode 100644
index 000000000..169ae3ac8
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
@@ -0,0 +1,19 @@
+package de.danoeh.antennapod.config;
+
+import de.danoeh.antennapod.core.ClientConfig;
+
+/**
+ * Configures the ClientConfig class of the core package.
+ */
+public class ClientConfigurator {
+
+ static {
+ ClientConfig.USER_AGENT = "AntennaPod/0.9.9.4";
+ ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl();
+ ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl();
+ ClientConfig.gpodnetCallbacks = new GpodnetCallbacksImpl();
+ ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl();
+ ClientConfig.storageCallbacks = new StorageCallbacksImpl();
+ ClientConfig.flattrCallbacks = new FlattrCallbacksImpl();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java
new file mode 100644
index 000000000..3a9e62435
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java
@@ -0,0 +1,60 @@
+package de.danoeh.antennapod.config;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import de.danoeh.antennapod.activity.DownloadAuthenticationActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.NavListAdapter;
+import de.danoeh.antennapod.core.DownloadServiceCallbacks;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+import de.danoeh.antennapod.fragment.DownloadsFragment;
+
+
+public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks {
+
+ @Override
+ public PendingIntent getNotificationContentIntent(Context context) {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV);
+ intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS);
+ Bundle args = new Bundle();
+ args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_RUNNING);
+ intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args);
+
+ return PendingIntent.getActivity(context, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ @Override
+ public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request) {
+ final Intent activityIntent = new Intent(context.getApplicationContext(), DownloadAuthenticationActivity.class);
+ activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, request);
+ activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true);
+ return PendingIntent.getActivity(context.getApplicationContext(), 0, activityIntent, PendingIntent.FLAG_ONE_SHOT);
+ }
+
+ @Override
+ public PendingIntent getReportNotificationContentIntent(Context context) {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV);
+ intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS);
+ Bundle args = new Bundle();
+ args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG);
+ intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args);
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ @Override
+ public void onFeedParsed(Context context, Feed feed) {
+ // do nothing
+ }
+
+ @Override
+ public boolean shouldCreateReport() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java
new file mode 100644
index 000000000..3817db6de
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java
@@ -0,0 +1,53 @@
+package de.danoeh.antennapod.config;
+
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.shredzone.flattr4j.oauth.AccessToken;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.activity.FlattrAuthActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.FlattrCallbacks;
+
+public class FlattrCallbacksImpl implements FlattrCallbacks {
+ private static final String TAG = "FlattrCallbacksImpl";
+
+ @Override
+ public boolean flattrEnabled() {
+ return true;
+ }
+
+ @Override
+ public Intent getFlattrAuthenticationActivityIntent(Context context) {
+ return new Intent(context, FlattrAuthActivity.class);
+ }
+
+ @Override
+ public PendingIntent getFlattrFailedNotificationContentIntent(Context context) {
+ return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0);
+ }
+
+ @Override
+ public String getFlattrAppKey() {
+ return BuildConfig.FLATTR_APP_KEY;
+ }
+
+ @Override
+ public String getFlattrAppSecret() {
+ return BuildConfig.FLATTR_APP_SECRET;
+ }
+
+ @Override
+ public void handleFlattrAuthenticationSuccess(AccessToken token) {
+ FlattrAuthActivity instance = FlattrAuthActivity.getInstance();
+ if (instance != null) {
+ instance.handleAuthenticationSuccess();
+ } else {
+ Log.e(TAG, "FlattrAuthActivity instance was null");
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java
new file mode 100644
index 000000000..5f8da6894
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java
@@ -0,0 +1,22 @@
+package de.danoeh.antennapod.config;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.GpodnetCallbacks;
+
+
+public class GpodnetCallbacksImpl implements GpodnetCallbacks {
+ @Override
+ public boolean gpodnetEnabled() {
+ return true;
+ }
+
+ @Override
+ public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context) {
+ return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java
new file mode 100644
index 000000000..997befe99
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.config;
+
+import android.content.Context;
+import android.content.Intent;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.AudioplayerActivity;
+import de.danoeh.antennapod.activity.VideoplayerActivity;
+import de.danoeh.antennapod.core.PlaybackServiceCallbacks;
+import de.danoeh.antennapod.core.feed.MediaType;
+
+
+public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks {
+ @Override
+ public Intent getPlayerActivityIntent(Context context, MediaType mediaType) {
+ if (mediaType == MediaType.VIDEO) {
+ return new Intent(context, VideoplayerActivity.class);
+ } else {
+ return new Intent(context, AudioplayerActivity.class);
+ }
+ }
+
+ @Override
+ public boolean useQueue() {
+ return true;
+ }
+
+ @Override
+ public int getNotificationIconResource(Context context) {
+ return R.drawable.ic_stat_antenna_default;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java
new file mode 100644
index 000000000..ec133aed1
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java
@@ -0,0 +1,107 @@
+package de.danoeh.antennapod.config;
+
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import de.danoeh.antennapod.core.StorageCallbacks;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+
+public class StorageCallbacksImpl implements StorageCallbacks {
+
+ @Override
+ public int getDatabaseVersion() {
+ return 12;
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to "
+ + newVersion + ".");
+ if (oldVersion <= 1) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN "
+ + PodDBAdapter.KEY_TYPE + " TEXT");
+ }
+ if (oldVersion <= 2) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS
+ + " ADD COLUMN " + PodDBAdapter.KEY_LINK + " TEXT");
+ }
+ if (oldVersion <= 3) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN " + PodDBAdapter.KEY_ITEM_IDENTIFIER + " TEXT");
+ }
+ if (oldVersion <= 4) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN "
+ + PodDBAdapter.KEY_FEED_IDENTIFIER + " TEXT");
+ }
+ if (oldVersion <= 5) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG
+ + " ADD COLUMN " + PodDBAdapter.KEY_REASON_DETAILED + " TEXT");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG
+ + " ADD COLUMN " + PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE + " TEXT");
+ }
+ if (oldVersion <= 6) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS
+ + " ADD COLUMN " + PodDBAdapter.KEY_CHAPTER_TYPE + " INTEGER");
+ }
+ if (oldVersion <= 7) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE
+ + " INTEGER");
+ }
+ if (oldVersion <= 8) {
+ final int KEY_ID_POSITION = 0;
+ final int KEY_MEDIA_POSITION = 1;
+
+ // Add feeditem column to feedmedia table
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_FEEDITEM
+ + " INTEGER");
+ Cursor feeditemCursor = db.query(PodDBAdapter.TABLE_NAME_FEED_ITEMS,
+ new String[]{PodDBAdapter.KEY_ID, PodDBAdapter.KEY_MEDIA}, "? > 0",
+ new String[]{PodDBAdapter.KEY_MEDIA}, null, null, null);
+ if (feeditemCursor.moveToFirst()) {
+ db.beginTransaction();
+ ContentValues contentValues = new ContentValues();
+ do {
+ long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION);
+ contentValues.put(PodDBAdapter.KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION));
+ db.update(PodDBAdapter.TABLE_NAME_FEED_MEDIA, contentValues, PodDBAdapter.KEY_ID + "=?", new String[]{String.valueOf(mediaId)});
+ contentValues.clear();
+ } while (feeditemCursor.moveToNext());
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+ feeditemCursor.close();
+ }
+ if (oldVersion <= 9) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD
+ + " INTEGER DEFAULT 1");
+ }
+ if (oldVersion <= 10) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS
+ + " INTEGER");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS
+ + " INTEGER");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
+ + " ADD COLUMN " + PodDBAdapter.KEY_PLAYED_DURATION
+ + " INTEGER");
+ }
+ if (oldVersion <= 11) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_USERNAME
+ + " TEXT");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_PASSWORD
+ + " TEXT");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE
+ + " INTEGER");
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java
new file mode 100644
index 000000000..bdb2d68ba
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import de.danoeh.antennapod.R;
+
+/**
+ * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
+ */
+public abstract class AuthenticationDialog extends Dialog {
+
+ private final int titleRes;
+ private final boolean enableUsernameField;
+ private final boolean showSaveCredentialsCheckbox;
+ private final String usernameInitialValue;
+ private final String passwordInitialValue;
+
+ public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, boolean showSaveCredentialsCheckbox, String usernameInitialValue, String passwordInitialValue) {
+ super(context);
+ this.titleRes = titleRes;
+ this.enableUsernameField = enableUsernameField;
+ this.showSaveCredentialsCheckbox = showSaveCredentialsCheckbox;
+ this.usernameInitialValue = usernameInitialValue;
+ this.passwordInitialValue = passwordInitialValue;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.authentication_dialog);
+ final EditText etxtUsername = (EditText) findViewById(R.id.etxtUsername);
+ final EditText etxtPassword = (EditText) findViewById(R.id.etxtPassword);
+ final CheckBox saveUsernamePassword = (CheckBox) findViewById(R.id.chkSaveUsernamePassword);
+ final Button butConfirm = (Button) findViewById(R.id.butConfirm);
+ final Button butCancel = (Button) findViewById(R.id.butCancel);
+
+ if (titleRes != 0) {
+ setTitle(titleRes);
+ } else {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+ etxtUsername.setEnabled(enableUsernameField);
+ if (showSaveCredentialsCheckbox) {
+ saveUsernamePassword.setVisibility(View.VISIBLE);
+ } else {
+ saveUsernamePassword.setVisibility(View.GONE);
+ }
+ if (usernameInitialValue != null) {
+ etxtUsername.setText(usernameInitialValue);
+ }
+ if (passwordInitialValue != null) {
+ etxtPassword.setText(passwordInitialValue);
+ }
+ setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onCancelled();
+ }
+ });
+ butCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancel();
+ }
+ });
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onConfirmed(etxtUsername.getText().toString(),
+ etxtPassword.getText().toString(),
+ showSaveCredentialsCheckbox && saveUsernamePassword.isChecked());
+ dismiss();
+ }
+ });
+ }
+
+ protected void onCancelled() {
+
+ }
+
+ protected abstract void onConfirmed(String username, String password, boolean saveUsernamePassword);
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java
new file mode 100644
index 000000000..1585f9b86
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java
@@ -0,0 +1,107 @@
+package de.danoeh.antennapod.dialog;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+/**
+ * Creates a new AlertDialog that displays preferences for auto-flattring to the user.
+ */
+public class AutoFlattrPreferenceDialog {
+
+ private AutoFlattrPreferenceDialog() {
+ }
+
+ public static void newAutoFlattrPreferenceDialog(final Activity activity, final AutoFlattrPreferenceDialogInterface callback) {
+ Validate.notNull(activity);
+ Validate.notNull(callback);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+
+ @SuppressLint("InflateParams") View view = activity.getLayoutInflater().inflate(R.layout.autoflattr_preference_dialog, null);
+ final CheckBox chkAutoFlattr = (CheckBox) view.findViewById(R.id.chkAutoFlattr);
+ final SeekBar skbPercent = (SeekBar) view.findViewById(R.id.skbPercent);
+ final TextView txtvStatus = (TextView) view.findViewById(R.id.txtvStatus);
+
+ chkAutoFlattr.setChecked(UserPreferences.isAutoFlattr());
+ skbPercent.setEnabled(chkAutoFlattr.isChecked());
+ txtvStatus.setEnabled(chkAutoFlattr.isChecked());
+
+ final int initialValue = (int) (UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100.0f);
+ setStatusMsgText(activity, txtvStatus, initialValue);
+ skbPercent.setProgress(initialValue);
+
+ chkAutoFlattr.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ skbPercent.setEnabled(chkAutoFlattr.isChecked());
+ txtvStatus.setEnabled(chkAutoFlattr.isChecked());
+ }
+ });
+
+ skbPercent.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ setStatusMsgText(activity, txtvStatus, progress);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+
+ builder.setTitle(R.string.pref_auto_flattr_title)
+ .setView(view)
+ .setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ float progDouble = ((float) skbPercent.getProgress()) / 100.0f;
+ callback.onConfirmed(chkAutoFlattr.isChecked(), progDouble);
+ dialog.dismiss();
+ }
+ })
+ .setNegativeButton(R.string.cancel_label, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ callback.onCancelled();
+ dialog.dismiss();
+ }
+ })
+ .setCancelable(false).show();
+ }
+
+ private static void setStatusMsgText(Context context, TextView txtvStatus, int progress) {
+ if (progress == 0) {
+ txtvStatus.setText(R.string.auto_flattr_ater_beginning);
+ } else if (progress == 100) {
+ txtvStatus.setText(R.string.auto_flattr_ater_end);
+ } else {
+ txtvStatus.setText(context.getString(R.string.auto_flattr_after_percent, progress));
+ }
+ }
+
+ public static interface AutoFlattrPreferenceDialogInterface {
+ public void onCancelled();
+
+ public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue);
+ }
+
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java
new file mode 100644
index 000000000..613cd1fec
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java
@@ -0,0 +1,434 @@
+package de.danoeh.antennapod.dialog;
+
+import android.annotation.TargetApi;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.widget.PopupMenu;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.Validate;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.DefaultActionButtonCallback;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.core.util.ShownotesProvider;
+import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
+
+/**
+ * Shows information about a specific FeedItem and provides actions like playing, downloading, etc.
+ */
+public class FeedItemDialog extends Dialog {
+ private static final String TAG = "FeedItemDialog";
+
+ private FeedItem item;
+ private QueueAccess queue;
+
+ private View header;
+ private TextView txtvTitle;
+ private WebView webvDescription;
+ private ImageButton butAction1;
+ private ImageButton butAction2;
+ private ImageButton butMore;
+ private PopupMenu popupMenu;
+
+ public static FeedItemDialog newInstance(Context context, FeedItemDialogSavedInstance savedInstance) {
+ Validate.notNull(savedInstance);
+ FeedItemDialog dialog = newInstance(context, savedInstance.item, savedInstance.queueAccess);
+ if (savedInstance.isShowing) {
+ dialog.show();
+ }
+ return dialog;
+ }
+
+ public static FeedItemDialog newInstance(Context context, FeedItem item, QueueAccess queue) {
+ if (useDarkThemeWorkAround()) {
+ return new FeedItemDialog(context, R.style.Theme_AntennaPod_Dark, item, queue);
+ } else {
+ return new FeedItemDialog(context, item, queue);
+ }
+ }
+
+ public FeedItemDialog(Context context, int theme, FeedItem item, QueueAccess queue) {
+ super(context, theme);
+ Validate.notNull(item);
+ Validate.notNull(queue);
+ this.item = item;
+ this.queue = queue;
+ }
+
+ private FeedItemDialog(Context context, FeedItem item, QueueAccess queue) {
+ this(context, 0, item, queue);
+ }
+
+ /**
+ * Returns true if the dialog should use a dark theme. This has to be done on Gingerbread devices
+ * because dialogs are only available in a dark theme.
+ */
+ private static boolean useDarkThemeWorkAround() {
+ return Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1
+ && UserPreferences.getTheme() != R.style.Theme_AntennaPod_Dark;
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.feeditem_dialog);
+
+ txtvTitle = (TextView) findViewById(R.id.txtvTitle);
+ header = findViewById(R.id.header);
+ webvDescription = (WebView) findViewById(R.id.webview);
+ butAction1 = (ImageButton) findViewById(R.id.butAction1);
+ butAction2 = (ImageButton) findViewById(R.id.butAction2);
+ butMore = (ImageButton) findViewById(R.id.butMoreActions);
+ popupMenu = new PopupMenu(getContext(), butMore);
+
+ webvDescription.setWebViewClient(new WebViewClient());
+
+ if (Build.VERSION.SDK_INT >= 14) { // ellipsize is causing problems on old versions, see #448
+ txtvTitle.setEllipsize(TextUtils.TruncateAt.END);
+ }
+
+ txtvTitle.setText(item.getTitle());
+
+ if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) {
+ if (Build.VERSION.SDK_INT >= 11
+ && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ webvDescription.setBackgroundColor(getContext().getResources().getColor(
+ R.color.black));
+ }
+ webvDescription.getSettings().setUseWideViewPort(false);
+ webvDescription.getSettings().setLayoutAlgorithm(
+ WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
+ webvDescription.getSettings().setLoadWithOverviewMode(true);
+ webvDescription.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ try {
+ getContext().startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+ });
+
+ loadDescriptionWebview(item);
+
+ butAction1.setOnClickListener(new View.OnClickListener() {
+ DefaultActionButtonCallback actionButtonCallback = new DefaultActionButtonCallback(getContext());
+
+ @Override
+
+ public void onClick(View v) {
+ actionButtonCallback.onActionButtonPressed(item);
+ FeedMedia media = item.getMedia();
+ if (media != null && media.isDownloaded()) {
+ // playback was started, dialog should close itself
+ dismiss();
+ }
+
+ }
+ }
+ );
+
+ butAction2.setOnClickListener(new View.OnClickListener()
+
+ {
+ @Override
+ public void onClick(View v) {
+ if (item.hasMedia()) {
+ FeedMedia media = item.getMedia();
+ if (!media.isDownloaded()) {
+ DBTasks.playMedia(getContext(), media, true, true, true);
+ dismiss();
+ } else {
+ DBWriter.deleteFeedMediaOfItem(getContext(), media.getId());
+ }
+ } else if (item.getLink() != null) {
+ Uri uri = Uri.parse(item.getLink());
+ getContext().startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+ }
+ }
+ );
+
+ butMore.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ popupMenu.getMenu().clear();
+ popupMenu.inflate(R.menu.feeditem_dialog);
+ if (item.hasMedia()) {
+ FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue);
+ } else {
+ // these are already available via button1 and button2
+ FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, queue,
+ R.id.mark_read_item, R.id.visit_website_item);
+ }
+ popupMenu.show();
+ }
+ }
+ );
+
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem menuItem) {
+
+ try {
+ return FeedItemMenuHandler.onMenuItemClicked(getContext(), menuItem.getItemId(), item);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_LONG).show();
+ return true;
+ }
+ }
+ }
+ );
+
+ updateMenuAppearance();
+ }
+
+
+ private final FeedItemMenuHandler.MenuInterface popupMenuInterface = new FeedItemMenuHandler.MenuInterface() {
+ @Override
+ public void setItemVisibility(int id, boolean visible) {
+ MenuItem item = popupMenu.getMenu().findItem(id);
+ if (item != null) {
+ item.setVisible(visible);
+ }
+ }
+ };
+
+ public void updateMenuAppearance() {
+ if (item == null || queue == null) {
+ Log.w(TAG, "UpdateMenuAppearance called while item or queue was null");
+ return;
+ }
+ FeedMedia media = item.getMedia();
+ if (media == null) {
+ TypedArray drawables = getContext().obtainStyledAttributes(new int[]{R.attr.navigation_accept,
+ R.attr.location_web_site});
+
+ if (!item.isRead()) {
+ butAction1.setImageDrawable(drawables.getDrawable(0));
+ butAction1.setContentDescription(getContext().getString(R.string.mark_read_label));
+ butAction1.setVisibility(View.VISIBLE);
+ } else {
+ butAction1.setVisibility(View.INVISIBLE);
+ }
+
+ if (item.getLink() != null) {
+ butAction2.setImageDrawable(drawables.getDrawable(1));
+ butAction2.setContentDescription(getContext().getString(R.string.visit_website_label));
+ } else {
+ butAction2.setEnabled(false);
+ }
+
+ drawables.recycle();
+ } else {
+ boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media);
+ TypedArray drawables = getContext().obtainStyledAttributes(new int[]{R.attr.av_play,
+ R.attr.av_download, R.attr.action_stream, R.attr.content_discard, R.attr.navigation_cancel});
+
+ if (!media.isDownloaded()) {
+ butAction2.setImageDrawable(drawables.getDrawable(2));
+ butAction2.setContentDescription(getContext().getString(R.string.stream_label));
+ } else {
+ butAction2.setImageDrawable(drawables.getDrawable(3));
+ butAction2.setContentDescription(getContext().getString(R.string.remove_episode_lable));
+ }
+
+ if (isDownloading) {
+ butAction1.setImageDrawable(drawables.getDrawable(4));
+ butAction1.setContentDescription(getContext().getString(R.string.cancel_download_label));
+ } else if (media.isDownloaded()) {
+ butAction1.setImageDrawable(drawables.getDrawable(0));
+ butAction1.setContentDescription(getContext().getString(R.string.play_label));
+ } else {
+ butAction1.setImageDrawable(drawables.getDrawable(1));
+ butAction1.setContentDescription(getContext().getString(R.string.download_label));
+ }
+
+ drawables.recycle();
+ }
+ }
+
+
+ private void loadDescriptionWebview(final ShownotesProvider shownotesProvider) {
+ AsyncTask<Void, Void, Void> loadTask = new AsyncTask<Void, Void, Void>() {
+ String data;
+
+
+ private String applyWebviewStyle(String textColor, String data) {
+ final String WEBVIEW_STYLE = "<html><head><style type=\"text/css\"> @font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }</style></head><body>%s</body></html>";
+ final int pageMargin = (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 8, getContext().getResources()
+ .getDisplayMetrics()
+ );
+ return String.format(WEBVIEW_STYLE, textColor, "100%", pageMargin,
+ pageMargin, pageMargin, pageMargin, data);
+ }
+
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ // /webvDescription.loadData(url, "text/html", "utf-8");
+ if (FeedItemDialog.this.isShowing() && webvDescription != null) {
+ webvDescription.loadDataWithBaseURL(null, data, "text/html",
+ "utf-8", "about:blank");
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Webview loaded");
+ }
+ }
+
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading Webview");
+ try {
+ Callable<String> shownotesLoadTask = shownotesProvider.loadShownotes();
+ final String shownotes = shownotesLoadTask.call();
+
+ data = StringEscapeUtils.unescapeHtml4(shownotes);
+ TypedArray res = getContext()
+ .getTheme()
+ .obtainStyledAttributes(
+ new int[]{android.R.attr.textColorPrimary});
+ int colorResource;
+ if (useDarkThemeWorkAround()) {
+ colorResource = getContext().getResources().getColor(R.color.black);
+ } else {
+ colorResource = res.getColor(0, 0);
+ }
+ String colorString = String.format("#%06X",
+ 0xFFFFFF & colorResource);
+ Log.i(TAG, "text color: " + colorString);
+ res.recycle();
+ data = applyWebviewStyle(colorString, data);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ };
+ loadTask.execute();
+ }
+
+ /**
+ * Convenience method that calls setQueue() and setItemFromCollection() with
+ * the given arguments.
+ *
+ * @return true if one of the calls to setItemFromCollection returned true,
+ * false otherwise.
+ */
+ public boolean updateContent(QueueAccess queue, List<FeedItem>... collections) {
+ setQueue(queue);
+
+ boolean setItemFromCollectionResult = false;
+ if (collections != null) {
+ for (List<FeedItem> list : collections) {
+ setItemFromCollectionResult |= setItemFromCollection(list);
+ }
+ }
+ if (isShowing()) {
+ updateMenuAppearance();
+ }
+
+ return setItemFromCollectionResult;
+ }
+
+
+ public void setItem(FeedItem item) {
+ Validate.notNull(item);
+ this.item = item;
+ }
+
+ /**
+ * Finds the FeedItem of this dialog in a collection and updates its state from that
+ * collection.
+ *
+ * @return true if the FeedItem was found, false otherwise.
+ */
+ public boolean setItemFromCollection(Collection<FeedItem> items) {
+ for (FeedItem item : items) {
+ if (item.getId() == this.item.getId()) {
+ setItem(item);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setQueue(QueueAccess queue) {
+ Validate.notNull(queue);
+ this.queue = queue;
+ }
+
+ public FeedItem getItem() {
+ return item;
+ }
+
+ public QueueAccess getQueue() {
+ return queue;
+ }
+
+ public FeedItemDialogSavedInstance save() {
+ return new FeedItemDialogSavedInstance(item, queue, isShowing());
+ }
+
+ /**
+ * Used to save the FeedItemDialog's state across configuration changes
+ */
+ public static class FeedItemDialogSavedInstance {
+ final FeedItem item;
+ final QueueAccess queueAccess;
+ final boolean isShowing;
+
+ private FeedItemDialogSavedInstance(FeedItem item, QueueAccess queueAccess, boolean isShowing) {
+ this.item = item;
+ this.queueAccess = queueAccess;
+ this.isShowing = isShowing;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java
new file mode 100644
index 000000000..16fb77f2a
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.dialog;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.Editable;
+import android.text.InputType;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+
+/**
+ * Creates a dialog that lets the user change the hostname for the gpodder.net service.
+ */
+public class GpodnetSetHostnameDialog {
+ private static final String TAG = "GpodnetSetHostnameDialog";
+
+ public static AlertDialog createDialog(final Context context) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(context);
+ final EditText et = new EditText(context);
+ et.setText(GpodnetPreferences.getHostname());
+ et.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
+ dialog.setTitle(R.string.pref_gpodnet_sethostname_title)
+ .setView(setupContentView(context, et))
+ .setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Editable e = et.getText();
+ if (e != null) {
+ GpodnetPreferences.setHostname(e.toString());
+ }
+ dialog.dismiss();
+ }
+ })
+ .setNegativeButton(R.string.cancel_label, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ })
+ .setNeutralButton(R.string.pref_gpodnet_sethostname_use_default_host, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST);
+ dialog.dismiss();
+ }
+ })
+ .setCancelable(true);
+ return dialog.show();
+ }
+
+ private static View setupContentView(Context context, EditText et) {
+ LinearLayout ll = new LinearLayout(context);
+ ll.addView(et);
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) et.getLayoutParams();
+ if (params != null) {
+ params.setMargins(8, 8, 8, 8);
+ params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+ params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ }
+ return ll;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java
new file mode 100644
index 000000000..6561d501e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java
@@ -0,0 +1,138 @@
+package de.danoeh.antennapod.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.*;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.R;
+
+import java.util.concurrent.TimeUnit;
+
+public abstract class TimeDialog extends Dialog {
+ private static final String TAG = "TimeDialog";
+
+ private static final int DEFAULT_SPINNER_POSITION = 1;
+
+ private Context context;
+
+ private EditText etxtTime;
+ private Spinner spTimeUnit;
+ private Button butConfirm;
+ private Button butCancel;
+
+ private TimeUnit[] units = {TimeUnit.SECONDS, TimeUnit.MINUTES,
+ TimeUnit.HOURS};
+
+ public TimeDialog(Context context, int titleTextId, int leftButtonTextId) {
+ super(context);
+ this.context = context;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ String[] spinnerContent = new String[]{context.getString(R.string.time_unit_seconds),
+ context.getString(R.string.time_unit_minutes),
+ context.getString(R.string.time_unit_hours)};
+
+ setContentView(R.layout.time_dialog);
+ etxtTime = (EditText) findViewById(R.id.etxtTime);
+ spTimeUnit = (Spinner) findViewById(R.id.spTimeUnit);
+ butConfirm = (Button) findViewById(R.id.butConfirm);
+ butCancel = (Button) findViewById(R.id.butCancel);
+
+ butConfirm.setText(R.string.set_sleeptimer_label);
+ butCancel.setText(R.string.cancel_label);
+ setTitle(R.string.set_sleeptimer_label);
+ ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(
+ this.getContext(), android.R.layout.simple_spinner_item,
+ spinnerContent);
+ spinnerAdapter
+ .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spTimeUnit.setAdapter(spinnerAdapter);
+ spTimeUnit.setSelection(DEFAULT_SPINNER_POSITION);
+ butCancel.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ }
+ });
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ try {
+ long input = readTimeMillis();
+ onTimeEntered(input);
+ dismiss();
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ Toast toast = Toast.makeText(context,
+ R.string.time_dialog_invalid_input,
+ Toast.LENGTH_LONG);
+ toast.show();
+ }
+ }
+ });
+ etxtTime.addTextChangedListener(new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ checkInputLength(s.length());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+
+ }
+ });
+ checkInputLength(etxtTime.getText().length());
+ etxtTime.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }, 100);
+
+
+
+ }
+
+ private void checkInputLength(int length) {
+ if (length > 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Length is larger than 0, enabling confirm button");
+ butConfirm.setEnabled(true);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Length is smaller than 0, disabling confirm button");
+ butConfirm.setEnabled(false);
+ }
+ }
+
+ public abstract void onTimeEntered(long millis);
+
+ private long readTimeMillis() {
+ TimeUnit selectedUnit = units[spTimeUnit.getSelectedItemPosition()];
+ long value = Long.valueOf(etxtTime.getText().toString());
+ return selectedUnit.toMillis(value);
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
new file mode 100644
index 000000000..8eba51540
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
@@ -0,0 +1,100 @@
+package de.danoeh.antennapod.dialog;
+
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class VariableSpeedDialog {
+ private VariableSpeedDialog() {
+ }
+
+ public static void showDialog(final Context context) {
+ if (com.aocate.media.MediaPlayer.isPrestoLibraryInstalled(context)) {
+ showSpeedSelectorDialog(context);
+ } else {
+ showGetPluginDialog(context);
+ }
+ }
+
+ private static void showGetPluginDialog(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.no_playback_plugin_title);
+ builder.setMessage(R.string.no_playback_plugin_msg);
+ builder.setNegativeButton(R.string.close_label, null);
+ builder.setPositiveButton(R.string.download_plugin_label,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ Intent playStoreIntent = new Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=com.falconware.prestissimo"));
+ context.startActivity(playStoreIntent);
+ } catch (ActivityNotFoundException e) {
+ // this is usually thrown on an emulator if the Android market is not installed
+ e.printStackTrace();
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ private static void showSpeedSelectorDialog(final Context context) {
+ final String[] speedValues = context.getResources().getStringArray(
+ R.array.playback_speed_values);
+ // According to Java spec these get initialized to false on creation
+ final boolean[] speedChecked = new boolean[speedValues.length];
+
+ // Build the "isChecked" array so that multiChoice dialog is
+ // populated correctly
+ List<String> selectedSpeedList = Arrays.asList(UserPreferences
+ .getPlaybackSpeedArray());
+ for (int i = 0; i < speedValues.length; i++) {
+ speedChecked[i] = selectedSpeedList.contains(speedValues[i]);
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.set_playback_speed_label);
+ builder.setMultiChoiceItems(R.array.playback_speed_values,
+ speedChecked, new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which,
+ boolean isChecked) {
+ speedChecked[which] = isChecked;
+ }
+
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int choiceCount = 0;
+ for (int i = 0; i < speedChecked.length; i++) {
+ if (speedChecked[i]) {
+ choiceCount++;
+ }
+ }
+ String[] newSpeedValues = new String[choiceCount];
+ int newSpeedIndex = 0;
+ for (int i = 0; i < speedChecked.length; i++) {
+ if (speedChecked[i]) {
+ newSpeedValues[newSpeedIndex++] = speedValues[i];
+ }
+ }
+
+ UserPreferences.setPlaybackSpeedArray(newSpeedValues);
+
+ }
+ });
+ builder.create().show();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
new file mode 100644
index 000000000..f5ae5a777
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
@@ -0,0 +1,76 @@
+package de.danoeh.antennapod.fragment;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
+import de.danoeh.antennapod.activity.OpmlImportFromPathActivity;
+import de.danoeh.antennapod.fragment.gpodnet.GpodnetMainFragment;
+
+/**
+ * Provides actions for adding new podcast subscriptions
+ */
+public class AddFeedFragment extends Fragment {
+ private static final String TAG = "AddFeedFragment";
+
+ /**
+ * Preset value for url text field.
+ */
+ public static final String ARG_FEED_URL = "feedurl";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View root = inflater.inflate(R.layout.addfeed, container, false);
+
+ final EditText etxtFeedurl = (EditText) root.findViewById(R.id.etxtFeedurl);
+
+ Bundle args = getArguments();
+ if (args != null && args.getString(ARG_FEED_URL) != null) {
+ etxtFeedurl.setText(args.getString(ARG_FEED_URL));
+ }
+
+ Button butBrowserGpoddernet = (Button) root.findViewById(R.id.butBrowseGpoddernet);
+ Button butOpmlImport = (Button) root.findViewById(R.id.butOpmlImport);
+ Button butConfirm = (Button) root.findViewById(R.id.butConfirm);
+
+ final MainActivity activity = (MainActivity) getActivity();
+ activity.getMainActivtyActionBar().setTitle(R.string.add_feed_label);
+
+ butBrowserGpoddernet.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ activity.loadChildFragment(new GpodnetMainFragment());
+ }
+ });
+
+ butOpmlImport.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(getActivity(),
+ OpmlImportFromPathActivity.class));
+ }
+ });
+
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class);
+ intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, etxtFeedurl.getText().toString());
+ intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, getString(R.string.add_feed_label));
+ startActivity(intent);
+ }
+ });
+
+ return root;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
new file mode 100644
index 000000000..21e4cbd80
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
@@ -0,0 +1,196 @@
+package de.danoeh.antennapod.fragment;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.view.View;
+import android.widget.ListView;
+import de.danoeh.antennapod.adapter.DownloadedEpisodesListAdapter;
+import de.danoeh.antennapod.dialog.FeedItemDialog;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.QueueAccess;
+
+import java.util.List;
+
+/**
+ * Displays all running downloads and provides a button to delete them
+ */
+public class CompletedDownloadsFragment extends ListFragment {
+ private static final int EVENTS =
+ EventDistributor.DOWNLOAD_HANDLED |
+ EventDistributor.DOWNLOADLOG_UPDATE |
+ EventDistributor.QUEUE_UPDATE |
+ EventDistributor.UNREAD_ITEMS_UPDATE;
+
+ private List<FeedItem> items;
+ private QueueAccess queue;
+ private DownloadedEpisodesListAdapter listAdapter;
+
+ private boolean viewCreated = false;
+ private boolean itemsLoaded = false;
+
+ private FeedItemDialog feedItemDialog;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ startItemLoader();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ stopItemLoader();
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ stopItemLoader();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ listAdapter = null;
+ viewCreated = false;
+ feedItemDialog = null;
+ stopItemLoader();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (viewCreated && itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ viewCreated = true;
+ if (itemsLoaded && getActivity() != null) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+ FeedItem item = listAdapter.getItem(position - l.getHeaderViewsCount());
+ if (item != null) {
+ feedItemDialog = FeedItemDialog.newInstance(getActivity(), item, queue);
+ feedItemDialog.show();
+ }
+
+ }
+
+ private void onFragmentLoaded() {
+ if (listAdapter == null) {
+ listAdapter = new DownloadedEpisodesListAdapter(getActivity(), itemAccess);
+ setListAdapter(listAdapter);
+ }
+ setListShown(true);
+ listAdapter.notifyDataSetChanged();
+ if (feedItemDialog != null) {
+ boolean res = feedItemDialog.updateContent(queue, items);
+ if (!res && feedItemDialog.isShowing()) {
+ feedItemDialog.dismiss();
+ }
+ }
+ }
+
+ private DownloadedEpisodesListAdapter.ItemAccess itemAccess = new DownloadedEpisodesListAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ return (items != null) ? items.size() : 0;
+ }
+
+ @Override
+ public FeedItem getItem(int position) {
+ return (items != null) ? items.get(position) : null;
+ }
+
+ @Override
+ public void onFeedItemSecondaryAction(FeedItem item) {
+ DBWriter.deleteFeedMediaOfItem(getActivity(), item.getMedia().getId());
+ }
+ };
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EventDistributor.DOWNLOAD_QUEUED) != 0) {
+ if (feedItemDialog != null && feedItemDialog.isShowing()) {
+ feedItemDialog.updateMenuAppearance();
+ }
+ } else if ((arg & EVENTS) != 0) {
+ startItemLoader();
+ }
+ }
+ };
+
+ private ItemLoader itemLoader;
+
+ private void startItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ itemLoader = new ItemLoader();
+ itemLoader.execute();
+ }
+
+ private void stopItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ }
+
+ private class ItemLoader extends AsyncTask<Void, Void, Object[]> {
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ if (!itemsLoaded && viewCreated) {
+ setListShown(false);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Object[] results) {
+ super.onPostExecute(results);
+ if (results != null) {
+ items = (List<FeedItem>) results[0];
+ queue = (QueueAccess) results[1];
+ itemsLoaded = true;
+ if (viewCreated && getActivity() != null) {
+ onFragmentLoaded();
+ }
+ }
+ }
+
+ @Override
+ protected Object[] doInBackground(Void... params) {
+ Context context = getActivity();
+ if (context != null) {
+ return new Object[]{DBReader.getDownloadedItems(context),
+ QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
new file mode 100644
index 000000000..69bd2b099
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
@@ -0,0 +1,105 @@
+package de.danoeh.antennapod.fragment;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+/**
+ * Displays the cover and the title of a FeedItem.
+ */
+public class CoverFragment extends Fragment implements
+ AudioplayerContentFragment {
+ private static final String TAG = "CoverFragment";
+ private static final String ARG_PLAYABLE = "arg.playable";
+
+ private Playable media;
+
+ private ImageView imgvCover;
+
+ private boolean viewCreated = false;
+
+ public static CoverFragment newInstance(Playable item) {
+ CoverFragment f = new CoverFragment();
+ if (item != null) {
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_PLAYABLE, item);
+ f.setArguments(args);
+ }
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ Bundle args = getArguments();
+ if (args != null) {
+ media = args.getParcelable(ARG_PLAYABLE);
+ } else {
+ Log.e(TAG, TAG + " was called with invalid arguments");
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.cover_fragment, container, false);
+ imgvCover = (ImageView) root.findViewById(R.id.imgvCover);
+ viewCreated = true;
+ return root;
+ }
+
+ private void loadMediaInfo() {
+ if (media != null) {
+ imgvCover.post(new Runnable() {
+
+ @Override
+ public void run() {
+ Context c = getActivity();
+ if (c != null) {
+ PicassoProvider.getMediaMetadataPicassoInstance(c)
+ .load(media.getImageUri())
+ .into(imgvCover);
+ }
+ }
+ });
+ } else {
+ Log.w(TAG, "loadMediaInfo was called while media was null");
+ }
+ }
+
+ @Override
+ public void onStart() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "On Start");
+ super.onStart();
+ if (media != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading media info");
+ loadMediaInfo();
+ } else {
+ Log.w(TAG, "Unable to load media info: media was null");
+ }
+ }
+
+ @Override
+ public void onDataSetChanged(Playable media) {
+ this.media = media;
+ if (viewCreated) {
+ loadMediaInfo();
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
new file mode 100644
index 000000000..9c7fade67
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
@@ -0,0 +1,121 @@
+package de.danoeh.antennapod.fragment;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.view.View;
+import de.danoeh.antennapod.adapter.DownloadLogAdapter;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.storage.DBReader;
+
+import java.util.List;
+
+/**
+ * Shows the download log
+ */
+public class DownloadLogFragment extends ListFragment {
+
+ private List<DownloadStatus> downloadLog;
+ private DownloadLogAdapter adapter;
+
+ private boolean viewsCreated = false;
+ private boolean itemsLoaded = false;
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ startItemLoader();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ stopItemLoader();
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ viewsCreated = true;
+ if (itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ private void onFragmentLoaded() {
+ if (adapter == null) {
+ adapter = new DownloadLogAdapter(getActivity(), itemAccess);
+ setListAdapter(adapter);
+ }
+ setListShown(true);
+ adapter.notifyDataSetChanged();
+
+ }
+
+ private DownloadLogAdapter.ItemAccess itemAccess = new DownloadLogAdapter.ItemAccess() {
+
+ @Override
+ public int getCount() {
+ return (downloadLog != null) ? downloadLog.size() : 0;
+ }
+
+ @Override
+ public DownloadStatus getItem(int position) {
+ return (downloadLog != null) ? downloadLog.get(position) : null;
+ }
+ };
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EventDistributor.DOWNLOADLOG_UPDATE) != 0) {
+ startItemLoader();
+ }
+ }
+ };
+
+ private ItemLoader itemLoader;
+
+ private void startItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ itemLoader = new ItemLoader();
+ itemLoader.execute();
+ }
+
+ private void stopItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ }
+
+ private class ItemLoader extends AsyncTask<Void, Void, List<DownloadStatus>> {
+
+ @Override
+ protected void onPostExecute(List<DownloadStatus> downloadStatuses) {
+ super.onPostExecute(downloadStatuses);
+ if (downloadStatuses != null) {
+ downloadLog = downloadStatuses;
+ itemsLoaded = true;
+ if (viewsCreated) {
+ onFragmentLoaded();
+ }
+ }
+ }
+
+ @Override
+ protected List<DownloadStatus> doInBackground(Void... params) {
+ Context context = getActivity();
+ if (context != null) {
+ return DBReader.getDownloadLog(context);
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
new file mode 100644
index 000000000..5a71cb36b
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
@@ -0,0 +1,145 @@
+package de.danoeh.antennapod.fragment;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+
+/**
+ * Shows the CompletedDownloadsFragment and the RunningDownloadsFragment
+ */
+public class DownloadsFragment extends Fragment {
+
+ public static final String ARG_SELECTED_TAB = "selected_tab";
+
+ public static final int POS_RUNNING = 0;
+ public static final int POS_COMPLETED = 1;
+ public static final int POS_LOG = 2;
+
+ private ViewPager pager;
+ private MainActivity activity;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View root = inflater.inflate(R.layout.pager_fragment, container, false);
+ pager = (ViewPager) root.findViewById(R.id.pager);
+ DownloadsPagerAdapter pagerAdapter = new DownloadsPagerAdapter(getChildFragmentManager(), getResources());
+ pager.setAdapter(pagerAdapter);
+ final ActionBar actionBar = activity.getMainActivtyActionBar();
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ ActionBar.TabListener tabListener = new ActionBar.TabListener() {
+ @Override
+ public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+ pager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+
+ @Override
+ public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+ };
+ actionBar.removeAllTabs();
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.downloads_running_label)
+ .setTabListener(tabListener));
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.downloads_completed_label)
+ .setTabListener(tabListener));
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.downloads_log_label)
+ .setTabListener(tabListener));
+
+ pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ actionBar.setSelectedNavigationItem(position);
+ }
+ });
+ return root;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (getArguments() != null) {
+ int tab = getArguments().getInt(ARG_SELECTED_TAB);
+ pager.setCurrentItem(tab, false);
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ this.activity = (MainActivity) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ activity.getMainActivtyActionBar().removeAllTabs();
+ activity.getMainActivtyActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+
+ public class DownloadsPagerAdapter extends FragmentPagerAdapter {
+
+
+
+
+ Resources resources;
+
+ public DownloadsPagerAdapter(FragmentManager fm, Resources resources) {
+ super(fm);
+ this.resources = resources;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case POS_RUNNING:
+ return new RunningDownloadsFragment();
+ case POS_COMPLETED:
+ return new CompletedDownloadsFragment();
+ case POS_LOG:
+ return new DownloadLogFragment();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return 3;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case POS_RUNNING:
+ return resources.getString(R.string.downloads_running_label);
+ case POS_COMPLETED:
+ return resources.getString(R.string.downloads_completed_label);
+ case POS_LOG:
+ return resources.getString(R.string.downloads_log_label);
+ default:
+ return super.getPageTitle(position);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
new file mode 100644
index 000000000..99320cffa
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
@@ -0,0 +1,238 @@
+package de.danoeh.antennapod.fragment;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlaybackController;
+
+/**
+ * Fragment which is supposed to be displayed outside of the MediaplayerActivity
+ * if the PlaybackService is running
+ */
+public class ExternalPlayerFragment extends Fragment {
+ private static final String TAG = "ExternalPlayerFragment";
+
+ private ViewGroup fragmentLayout;
+ private ImageView imgvCover;
+ private ViewGroup layoutInfo;
+ private TextView txtvTitle;
+ private ImageButton butPlay;
+
+ private PlaybackController controller;
+
+ public ExternalPlayerFragment() {
+ super();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.external_player_fragment,
+ container, false);
+ fragmentLayout = (ViewGroup) root.findViewById(R.id.fragmentLayout);
+ imgvCover = (ImageView) root.findViewById(R.id.imgvCover);
+ layoutInfo = (ViewGroup) root.findViewById(R.id.layoutInfo);
+ txtvTitle = (TextView) root.findViewById(R.id.txtvTitle);
+ butPlay = (ImageButton) root.findViewById(R.id.butPlay);
+
+ layoutInfo.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "layoutInfo was clicked");
+
+ if (controller.getMedia() != null) {
+ startActivity(PlaybackService.getPlayerActivityIntent(
+ getActivity(), controller.getMedia()));
+ }
+ }
+ });
+ return root;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ controller = setupPlaybackController();
+ butPlay.setOnClickListener(controller.newOnPlayButtonClickListener());
+ }
+
+ private PlaybackController setupPlaybackController() {
+ return new PlaybackController(getActivity(), true) {
+
+ @Override
+ public void setupGUI() {
+ }
+
+ @Override
+ public void onPositionObserverUpdate() {
+ }
+
+ @Override
+ public void onReloadNotification(int code) {
+ }
+
+ @Override
+ public void onBufferStart() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onBufferEnd() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onBufferUpdate(float progress) {
+ }
+
+ @Override
+ public void onSleepTimerUpdate() {
+ }
+
+ @Override
+ public void handleError(int code) {
+ }
+
+ @Override
+ public ImageButton getPlayButton() {
+ return butPlay;
+ }
+
+ @Override
+ public void postStatusMsg(int msg) {
+ }
+
+ @Override
+ public void clearStatusMsg() {
+ }
+
+ @Override
+ public boolean loadMediaInfo() {
+ ExternalPlayerFragment fragment = ExternalPlayerFragment.this;
+ if (fragment != null) {
+ return fragment.loadMediaInfo();
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onAwaitingVideoSurface() {
+ }
+
+ @Override
+ public void onServiceQueried() {
+ }
+
+ @Override
+ public void onShutdownNotification() {
+ if (fragmentLayout != null) {
+ fragmentLayout.setVisibility(View.GONE);
+ }
+ controller = setupPlaybackController();
+ if (butPlay != null) {
+ butPlay.setOnClickListener(controller
+ .newOnPlayButtonClickListener());
+ }
+
+ }
+
+ @Override
+ public void onPlaybackEnd() {
+ if (fragmentLayout != null) {
+ fragmentLayout.setVisibility(View.GONE);
+ }
+ controller = setupPlaybackController();
+ if (butPlay != null) {
+ butPlay.setOnClickListener(controller
+ .newOnPlayButtonClickListener());
+ }
+ }
+
+ @Override
+ public void onPlaybackSpeedChange() {
+ // TODO Auto-generated method stub
+
+ }
+ };
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ controller.init();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Fragment is about to be destroyed");
+ if (controller != null) {
+ controller.release();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (controller != null) {
+ controller.pause();
+ }
+ }
+
+ private boolean loadMediaInfo() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading media info");
+ if (controller.serviceAvailable()) {
+ Playable media = controller.getMedia();
+ if (media != null) {
+ txtvTitle.setText(media.getEpisodeTitle());
+
+ PicassoProvider.getMediaMetadataPicassoInstance(getActivity())
+ .load(media.getImageUri())
+ .fit()
+ .into(imgvCover);
+
+ fragmentLayout.setVisibility(View.VISIBLE);
+ if (controller.isPlayingVideo()) {
+ butPlay.setVisibility(View.GONE);
+ } else {
+ butPlay.setVisibility(View.VISIBLE);
+ }
+ return true;
+ } else {
+ Log.w(TAG,
+ "loadMediaInfo was called while the media object of playbackService was null!");
+ return false;
+ }
+ } else {
+ Log.w(TAG,
+ "loadMediaInfo was called while playbackService was null!");
+ return false;
+ }
+ }
+
+ private String getPositionString(int position, int duration) {
+ return Converter.getDurationStringLong(position) + " / "
+ + Converter.getDurationStringLong(duration);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
new file mode 100644
index 000000000..f0e73a372
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
@@ -0,0 +1,476 @@
+package de.danoeh.antennapod.fragment;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebSettings.LayoutAlgorithm;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Toast;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.ShareUtils;
+import de.danoeh.antennapod.core.util.ShownotesProvider;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlaybackController;
+import de.danoeh.antennapod.core.util.playback.Timeline;
+
+/**
+ * Displays the description of a Playable object in a Webview.
+ */
+public class ItemDescriptionFragment extends Fragment {
+
+ private static final String TAG = "ItemDescriptionFragment";
+
+ private static final String PREF = "ItemDescriptionFragmentPrefs";
+ private static final String PREF_SCROLL_Y = "prefScrollY";
+ private static final String PREF_PLAYABLE_ID = "prefPlayableId";
+
+ private static final String ARG_PLAYABLE = "arg.playable";
+ private static final String ARG_FEEDITEM_ID = "arg.feeditem";
+
+ private static final String ARG_SAVE_STATE = "arg.saveState";
+ private static final String ARG_HIGHLIGHT_TIMECODES = "arg.highlightTimecodes";
+
+ private WebView webvDescription;
+
+ private ShownotesProvider shownotesProvider;
+ private Playable media;
+
+
+ private AsyncTask<Void, Void, Void> webViewLoader;
+
+ /**
+ * URL that was selected via long-press.
+ */
+ private String selectedURL;
+
+ /**
+ * True if Fragment should save its state (e.g. scrolling position) in a
+ * shared preference.
+ */
+ private boolean saveState;
+
+ /**
+ * True if Fragment should highlight timecodes (e.g. time codes in the HH:MM:SS format).
+ */
+ private boolean highlightTimecodes;
+
+ public static ItemDescriptionFragment newInstance(Playable media,
+ boolean saveState,
+ boolean highlightTimecodes) {
+ ItemDescriptionFragment f = new ItemDescriptionFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_PLAYABLE, media);
+ args.putBoolean(ARG_SAVE_STATE, saveState);
+ args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes);
+ f.setArguments(args);
+ return f;
+ }
+
+ public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState, boolean highlightTimecodes) {
+ ItemDescriptionFragment f = new ItemDescriptionFragment();
+ Bundle args = new Bundle();
+ args.putLong(ARG_FEEDITEM_ID, item.getId());
+ args.putBoolean(ARG_SAVE_STATE, saveState);
+ args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes);
+ f.setArguments(args);
+ return f;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Creating view");
+ webvDescription = new WebView(getActivity());
+ if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) {
+ if (Build.VERSION.SDK_INT >= 11
+ && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ webvDescription.setBackgroundColor(getResources().getColor(
+ R.color.black));
+ }
+ webvDescription.getSettings().setUseWideViewPort(false);
+ webvDescription.getSettings().setLayoutAlgorithm(
+ LayoutAlgorithm.NARROW_COLUMNS);
+ webvDescription.getSettings().setLoadWithOverviewMode(true);
+ webvDescription.setOnLongClickListener(webViewLongClickListener);
+ webvDescription.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (Timeline.isTimecodeLink(url)) {
+ onTimecodeLinkSelected(url);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ e.printStackTrace();
+ return true;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Page finished");
+ // Restoring the scroll position might not always work
+ view.postDelayed(new Runnable() {
+
+ @Override
+ public void run() {
+ restoreFromPreference();
+ }
+
+ }, 50);
+ }
+
+ });
+
+ registerForContextMenu(webvDescription);
+ return webvDescription;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Fragment attached");
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Fragment detached");
+ if (webViewLoader != null) {
+ webViewLoader.cancel(true);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Fragment destroyed");
+ if (webViewLoader != null) {
+ webViewLoader.cancel(true);
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Creating fragment");
+ Bundle args = getArguments();
+ saveState = args.getBoolean(ARG_SAVE_STATE, false);
+ highlightTimecodes = args.getBoolean(ARG_HIGHLIGHT_TIMECODES, false);
+
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ Bundle args = getArguments();
+ if (args.containsKey(ARG_PLAYABLE)) {
+ media = args.getParcelable(ARG_PLAYABLE);
+ shownotesProvider = media;
+ startLoader();
+ } else if (args.containsKey(ARG_FEEDITEM_ID)) {
+ AsyncTask<Void, Void, FeedItem> itemLoadTask = new AsyncTask<Void, Void, FeedItem>() {
+
+ @Override
+ protected FeedItem doInBackground(Void... voids) {
+ return DBReader.getFeedItem(getActivity(), getArguments().getLong(ARG_FEEDITEM_ID));
+ }
+
+ @Override
+ protected void onPostExecute(FeedItem feedItem) {
+ super.onPostExecute(feedItem);
+ shownotesProvider = feedItem;
+ startLoader();
+ }
+ };
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ itemLoadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ itemLoadTask.execute();
+ }
+ }
+
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @SuppressLint("NewApi")
+ private void startLoader() {
+ webViewLoader = createLoader();
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ webViewLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ webViewLoader.execute();
+ }
+ }
+
+ private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ WebView.HitTestResult r = webvDescription.getHitTestResult();
+ if (r != null
+ && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Link of webview was long-pressed. Extra: "
+ + r.getExtra()
+ );
+ selectedURL = r.getExtra();
+ webvDescription.showContextMenu();
+ return true;
+ }
+ selectedURL = null;
+ return false;
+ }
+ };
+
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ boolean handled = selectedURL != null;
+ if (selectedURL != null) {
+ switch (item.getItemId()) {
+ case R.id.open_in_browser_item:
+ Uri uri = Uri.parse(selectedURL);
+ getActivity()
+ .startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ break;
+ case R.id.share_url_item:
+ ShareUtils.shareLink(getActivity(), selectedURL);
+ break;
+ case R.id.copy_url_item:
+ if (android.os.Build.VERSION.SDK_INT >= 11) {
+ ClipData clipData = ClipData.newPlainText(selectedURL,
+ selectedURL);
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) getActivity()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(clipData);
+ } else {
+ android.text.ClipboardManager cm = (android.text.ClipboardManager) getActivity()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setText(selectedURL);
+ }
+ Toast t = Toast.makeText(getActivity(),
+ R.string.copied_url_msg, Toast.LENGTH_SHORT);
+ t.show();
+ break;
+ case R.id.go_to_position_item:
+ if (Timeline.isTimecodeLink(selectedURL)) {
+ onTimecodeLinkSelected(selectedURL);
+ } else {
+ Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedURL);
+ }
+ break;
+ default:
+ handled = false;
+ break;
+
+ }
+ selectedURL = null;
+ }
+ return handled;
+
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ if (selectedURL != null) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ if (Timeline.isTimecodeLink(selectedURL)) {
+ menu.add(Menu.NONE, R.id.go_to_position_item, Menu.NONE,
+ R.string.go_to_position_label);
+ menu.setHeaderTitle(Converter.getDurationStringLong(Timeline.getTimecodeLinkTime(selectedURL)));
+ } else {
+ menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE,
+ R.string.open_in_browser_label);
+ menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE,
+ R.string.copy_url_label);
+ menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE,
+ R.string.share_url_label);
+ menu.setHeaderTitle(selectedURL);
+ }
+ }
+ }
+
+ private AsyncTask<Void, Void, Void> createLoader() {
+ return new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ if (getActivity() != null) {
+ ((ActionBarActivity) getActivity())
+ .setSupportProgressBarIndeterminateVisibility(false);
+ }
+ webViewLoader = null;
+ }
+
+ String data;
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ // /webvDescription.loadData(url, "text/html", "utf-8");
+ webvDescription.loadDataWithBaseURL(null, data, "text/html",
+ "utf-8", "about:blank");
+ if (getActivity() != null) {
+ ((ActionBarActivity) getActivity())
+ .setSupportProgressBarIndeterminateVisibility(false);
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Webview loaded");
+ webViewLoader = null;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ if (getActivity() != null) {
+ ((ActionBarActivity) getActivity())
+ .setSupportProgressBarIndeterminateVisibility(false);
+ }
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading Webview");
+ try {
+ Activity activity = getActivity();
+ if (activity != null) {
+ Timeline timeline = new Timeline(activity, shownotesProvider);
+ data = timeline.processShownotes(highlightTimecodes);
+ } else {
+ cancel(true);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ };
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ savePreference();
+ }
+
+ private void savePreference() {
+ if (saveState) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Saving preferences");
+ SharedPreferences prefs = getActivity().getSharedPreferences(PREF,
+ Activity.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ if (media != null && webvDescription != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Saving scroll position: "
+ + webvDescription.getScrollY()
+ );
+ editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY());
+ editor.putString(PREF_PLAYABLE_ID, media.getIdentifier()
+ .toString());
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "savePreferences was called while media or webview was null");
+ editor.putInt(PREF_SCROLL_Y, -1);
+ editor.putString(PREF_PLAYABLE_ID, "");
+ }
+ editor.commit();
+ }
+ }
+
+ private boolean restoreFromPreference() {
+ if (saveState) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Restoring from preferences");
+ Activity activity = getActivity();
+ if (activity != null) {
+ SharedPreferences prefs = activity.getSharedPreferences(
+ PREF, Activity.MODE_PRIVATE);
+ String id = prefs.getString(PREF_PLAYABLE_ID, "");
+ int scrollY = prefs.getInt(PREF_SCROLL_Y, -1);
+ if (scrollY != -1 && media != null
+ && id.equals(media.getIdentifier().toString())
+ && webvDescription != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Restored scroll Position: " + scrollY);
+ webvDescription.scrollTo(webvDescription.getScrollX(),
+ scrollY);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private void onTimecodeLinkSelected(String link) {
+ int time = Timeline.getTimecodeLinkTime(link);
+ if (getActivity() != null && getActivity() instanceof ItemDescriptionFragmentCallback) {
+ PlaybackController pc = ((ItemDescriptionFragmentCallback) getActivity()).getPlaybackController();
+ if (pc != null) {
+ pc.seekTo(time);
+ }
+ }
+ }
+
+ public interface ItemDescriptionFragmentCallback {
+ public PlaybackController getPlaybackController();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java
new file mode 100644
index 000000000..9eaeb56dd
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java
@@ -0,0 +1,456 @@
+package de.danoeh.antennapod.fragment;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.ListFragment;
+import android.support.v7.app.ActionBarActivity;
+import android.support.v7.widget.SearchView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.Validate;
+
+import java.util.List;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.FeedInfoActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.DefaultActionButtonCallback;
+import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
+import de.danoeh.antennapod.core.asynctask.DownloadObserver;
+import de.danoeh.antennapod.core.asynctask.FeedRemover;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
+import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
+import de.danoeh.antennapod.dialog.FeedItemDialog;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+/**
+ * Displays a list of FeedItems.
+ */
+@SuppressLint("ValidFragment")
+public class ItemlistFragment extends ListFragment {
+ private static final String TAG = "ItemlistFragment";
+
+ private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED
+ | EventDistributor.DOWNLOAD_QUEUED
+ | EventDistributor.QUEUE_UPDATE
+ | EventDistributor.UNREAD_ITEMS_UPDATE;
+
+ public static final String EXTRA_SELECTED_FEEDITEM = "extra.de.danoeh.antennapod.activity.selected_feeditem";
+ public static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id";
+
+ protected FeedItemlistAdapter adapter;
+
+ private long feedID;
+ private Feed feed;
+ protected QueueAccess queue;
+
+ private boolean itemsLoaded = false;
+ private boolean viewsCreated = false;
+
+ private DownloadObserver downloadObserver;
+ private List<Downloader> downloaderList;
+
+ private FeedItemDialog feedItemDialog;
+ private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance;
+
+
+ /**
+ * Creates new ItemlistFragment which shows the Feeditems of a specific
+ * feed. Sets 'showFeedtitle' to false
+ *
+ * @param feedId The id of the feed to show
+ * @return the newly created instance of an ItemlistFragment
+ */
+ public static ItemlistFragment newInstance(long feedId) {
+ ItemlistFragment i = new ItemlistFragment();
+ Bundle b = new Bundle();
+ b.putLong(ARGUMENT_FEED_ID, feedId);
+ i.setArguments(b);
+ return i;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ setHasOptionsMenu(true);
+
+ Bundle args = getArguments();
+ Validate.notNull(args);
+ feedID = args.getLong(ARGUMENT_FEED_ID);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ if (downloadObserver != null) {
+ downloadObserver.setActivity(getActivity());
+ downloadObserver.onResume();
+ }
+ if (viewsCreated && itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ stopItemLoader();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateProgressBarVisibility();
+ startItemLoader();
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ stopItemLoader();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ resetViewState();
+ }
+
+ private void resetViewState() {
+ adapter = null;
+ viewsCreated = false;
+ if (downloadObserver != null) {
+ downloadObserver.onPause();
+ }
+ if (feedItemDialog != null) {
+ feedItemDialogSavedInstance = feedItemDialog.save();
+ }
+ feedItemDialog = null;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ FeedMenuHandler.onCreateOptionsMenu(inflater, menu);
+
+ final SearchView sv = new SearchView(getActivity());
+ MenuItemUtils.addSearchItem(menu, sv);
+ sv.setQueryHint(getString(R.string.search_hint));
+ sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ sv.clearFocus();
+ if (itemsLoaded) {
+ ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s, feed.getId()));
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ return false;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ FeedMenuHandler.onPrepareOptionsMenu(menu, feed);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (!super.onOptionsItemSelected(item)) {
+ try {
+ if (!FeedMenuHandler.onOptionsItemClicked(getActivity(), item, feed)) {
+ switch (item.getItemId()) {
+ case R.id.remove_item:
+ final FeedRemover remover = new FeedRemover(
+ getActivity(), feed) {
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ ((MainActivity) getActivity()).loadNavFragment(MainActivity.POS_NEW, null);
+ }
+ };
+ ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(),
+ R.string.remove_feed_label,
+ R.string.feed_delete_confirmation_msg) {
+
+ @Override
+ public void onConfirmButtonPressed(
+ DialogInterface dialog) {
+ dialog.dismiss();
+ remover.executeAsync();
+ }
+ };
+ conDialog.createNewDialog().show();
+ return true;
+ default:
+ return false;
+
+ }
+ } else {
+ return true;
+ }
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage());
+ return true;
+ }
+ } else {
+ return true;
+ }
+
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle("");
+
+ viewsCreated = true;
+ if (itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ FeedItem selection = adapter.getItem(position - l.getHeaderViewsCount());
+ feedItemDialog = FeedItemDialog.newInstance(getActivity(), selection, queue);
+ feedItemDialog.show();
+ }
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EVENTS & arg) != 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received contentUpdate Intent.");
+ if ((EventDistributor.DOWNLOAD_QUEUED & arg) != 0) {
+ updateProgressBarVisibility();
+ } else {
+ startItemLoader();
+ updateProgressBarVisibility();
+ }
+ }
+ }
+ };
+
+ private void updateProgressBarVisibility() {
+ if (feed != null) {
+ if (DownloadService.isRunning
+ && DownloadRequester.getInstance().isDownloadingFile(feed)) {
+ ((ActionBarActivity) getActivity())
+ .setSupportProgressBarIndeterminateVisibility(true);
+ } else {
+ ((ActionBarActivity) getActivity())
+ .setSupportProgressBarIndeterminateVisibility(false);
+ }
+ getActivity().supportInvalidateOptionsMenu();
+ }
+ }
+
+ private void onFragmentLoaded() {
+ if (adapter == null) {
+ getListView().setAdapter(null);
+ setupHeaderView();
+ adapter = new FeedItemlistAdapter(getActivity(), itemAccess, new DefaultActionButtonCallback(getActivity()), false);
+ setListAdapter(adapter);
+ downloadObserver = new DownloadObserver(getActivity(), new Handler(), downloadObserverCallback);
+ downloadObserver.onResume();
+ }
+ setListShown(true);
+ adapter.notifyDataSetChanged();
+
+ if (feedItemDialog != null) {
+ feedItemDialog.updateContent(queue, feed.getItems());
+ } else if (feedItemDialogSavedInstance != null) {
+ feedItemDialog = FeedItemDialog.newInstance(getActivity(), feedItemDialogSavedInstance);
+ }
+ getActivity().supportInvalidateOptionsMenu();
+ }
+
+ private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() {
+ @Override
+ public void onContentChanged() {
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ }
+ if (feedItemDialog != null && feedItemDialog.isShowing()) {
+ feedItemDialog.updateMenuAppearance();
+ }
+ }
+
+ @Override
+ public void onDownloadDataAvailable(List<Downloader> downloaderList) {
+ ItemlistFragment.this.downloaderList = downloaderList;
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ private void setupHeaderView() {
+ if (getListView() == null || feed == null) {
+ Log.e(TAG, "Unable to setup listview: listView = null or feed = null");
+ return;
+ }
+ ListView lv = getListView();
+ LayoutInflater inflater = (LayoutInflater)
+ getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View header = inflater.inflate(R.layout.feeditemlist_header, lv, false);
+ lv.addHeaderView(header);
+
+ TextView txtvTitle = (TextView) header.findViewById(R.id.txtvTitle);
+ TextView txtvAuthor = (TextView) header.findViewById(R.id.txtvAuthor);
+ ImageView imgvCover = (ImageView) header.findViewById(R.id.imgvCover);
+ ImageButton butShowInfo = (ImageButton) header.findViewById(R.id.butShowInfo);
+ ImageButton butVisitWebsite = (ImageButton) header.findViewById(R.id.butVisitWebsite);
+
+ txtvTitle.setText(feed.getTitle());
+ txtvAuthor.setText(feed.getAuthor());
+
+ PicassoProvider.getDefaultPicassoInstance(getActivity())
+ .load(feed.getImageUri())
+ .fit()
+ .into(imgvCover);
+
+ if (feed.getLink() == null) {
+ butVisitWebsite.setVisibility(View.INVISIBLE);
+ } else {
+ butVisitWebsite.setVisibility(View.VISIBLE);
+ butVisitWebsite.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Uri uri = Uri.parse(feed.getLink());
+ startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+ });
+ }
+ butShowInfo.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (viewsCreated && itemsLoaded) {
+ Intent startIntent = new Intent(getActivity(), FeedInfoActivity.class);
+ startIntent.putExtra(FeedInfoActivity.EXTRA_FEED_ID,
+ feed.getId());
+ startActivity(startIntent);
+ }
+ }
+ });
+ }
+
+ private FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() {
+
+ @Override
+ public FeedItem getItem(int position) {
+ return (feed != null) ? feed.getItemAtIndex(true, position) : null;
+ }
+
+ @Override
+ public int getCount() {
+ return (feed != null) ? feed.getNumOfItems(true) : 0;
+ }
+
+ @Override
+ public boolean isInQueue(FeedItem item) {
+ return (queue != null) && queue.contains(item.getId());
+ }
+
+ @Override
+ public int getItemDownloadProgressPercent(FeedItem item) {
+ if (downloaderList != null) {
+ for (Downloader downloader : downloaderList) {
+ if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) {
+ return downloader.getDownloadRequest().getProgressPercent();
+ }
+ }
+ }
+ return 0;
+ }
+ };
+
+ private ItemLoader itemLoader;
+
+ private void startItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ itemLoader = new ItemLoader();
+ itemLoader.execute(feedID);
+ }
+
+ private void stopItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ }
+
+ private class ItemLoader extends AsyncTask<Long, Void, Object[]> {
+ @Override
+ protected Object[] doInBackground(Long... params) {
+ long feedID = params[0];
+ Context context = getActivity();
+ if (context != null) {
+ return new Object[]{DBReader.getFeed(context, feedID),
+ QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Object[] res) {
+ super.onPostExecute(res);
+ if (res != null) {
+ feed = (Feed) res[0];
+ queue = (QueueAccess) res[1];
+ itemsLoaded = true;
+ if (viewsCreated) {
+ onFragmentLoaded();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java
new file mode 100644
index 000000000..d126f2980
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java
@@ -0,0 +1,425 @@
+package de.danoeh.antennapod.fragment;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.ActionBarActivity;
+import android.support.v7.widget.SearchView;
+import android.view.*;
+import android.widget.AdapterView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.mobeta.android.dslv.DragSortListView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.DefaultActionButtonCallback;
+import de.danoeh.antennapod.adapter.NewEpisodesListAdapter;
+import de.danoeh.antennapod.core.asynctask.DownloadObserver;
+import de.danoeh.antennapod.dialog.FeedItemDialog;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Shows unread or recently published episodes
+ */
+public class NewEpisodesFragment extends Fragment {
+ private static final String TAG = "NewEpisodesFragment";
+ private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED |
+ EventDistributor.DOWNLOAD_QUEUED |
+ EventDistributor.QUEUE_UPDATE |
+ EventDistributor.UNREAD_ITEMS_UPDATE;
+
+ private static final int RECENT_EPISODES_LIMIT = 150;
+ private static final String PREF_NAME = "PrefNewEpisodesFragment";
+ private static final String PREF_EPISODE_FILTER_BOOL = "newEpisodeFilterEnabled";
+
+
+ private DragSortListView listView;
+ private NewEpisodesListAdapter listAdapter;
+ private TextView txtvEmpty;
+ private ProgressBar progLoading;
+
+ private List<FeedItem> unreadItems;
+ private List<FeedItem> recentItems;
+ private QueueAccess queueAccess;
+ private List<Downloader> downloaderList;
+
+ private boolean itemsLoaded = false;
+ private boolean viewsCreated = false;
+ private boolean showOnlyNewEpisodes = false;
+
+ private AtomicReference<MainActivity> activity = new AtomicReference<MainActivity>();
+
+ private DownloadObserver downloadObserver = null;
+
+ private FeedItemDialog feedItemDialog;
+ private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ setHasOptionsMenu(true);
+
+ updateShowOnlyEpisodes();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ startItemLoader();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ this.activity.set((MainActivity) getActivity());
+ if (downloadObserver != null) {
+ downloadObserver.setActivity(getActivity());
+ downloadObserver.onResume();
+ }
+ if (viewsCreated && itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ stopItemLoader();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ this.activity.set((MainActivity) getActivity());
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ resetViewState();
+ }
+
+ private void resetViewState() {
+ listAdapter = null;
+ activity.set(null);
+ viewsCreated = false;
+ if (downloadObserver != null) {
+ downloadObserver.onPause();
+ }
+ if (feedItemDialog != null) {
+ feedItemDialogSavedInstance = feedItemDialog.save();
+ }
+ feedItemDialog = null;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ inflater.inflate(R.menu.new_episodes, menu);
+
+ final SearchView sv = new SearchView(getActivity());
+ MenuItemUtils.addSearchItem(menu, sv);
+ sv.setQueryHint(getString(R.string.search_hint));
+ sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ sv.clearFocus();
+ ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s));
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ return false;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ menu.findItem(R.id.mark_all_read_item).setVisible(unreadItems != null && !unreadItems.isEmpty());
+ menu.findItem(R.id.episode_filter_item).setChecked(showOnlyNewEpisodes);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (!super.onOptionsItemSelected(item)) {
+ switch (item.getItemId()) {
+ case R.id.refresh_item:
+ List<Feed> feeds = ((MainActivity) getActivity()).getFeeds();
+ if (feeds != null) {
+ DBTasks.refreshAllFeeds(getActivity(), feeds);
+ }
+ return true;
+ case R.id.mark_all_read_item:
+ DBWriter.markAllItemsRead(getActivity());
+ Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show();
+ return true;
+ case R.id.episode_filter_item:
+ boolean newVal = !item.isChecked();
+ setShowOnlyNewEpisodes(newVal);
+ item.setChecked(newVal);
+ return true;
+ default:
+ return false;
+ }
+ } else {
+ return true;
+ }
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.all_episodes_label);
+
+ View root = inflater.inflate(R.layout.new_episodes_fragment, container, false);
+
+ listView = (DragSortListView) root.findViewById(android.R.id.list);
+ txtvEmpty = (TextView) root.findViewById(android.R.id.empty);
+ progLoading = (ProgressBar) root.findViewById(R.id.progLoading);
+
+ listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ FeedItem item = (FeedItem) listAdapter.getItem(position - listView.getHeaderViewsCount());
+ if (item != null) {
+ feedItemDialog = FeedItemDialog.newInstance(activity.get(), item, queueAccess);
+ feedItemDialog.show();
+ }
+
+ }
+ });
+
+ final int secondColor = (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) ? R.color.swipe_refresh_secondary_color_dark : R.color.swipe_refresh_secondary_color_light;
+
+ if (!itemsLoaded) {
+ progLoading.setVisibility(View.VISIBLE);
+ txtvEmpty.setVisibility(View.GONE);
+ }
+
+ viewsCreated = true;
+
+ if (itemsLoaded && activity.get() != null) {
+ onFragmentLoaded();
+ }
+
+ return root;
+ }
+
+ private void onFragmentLoaded() {
+ if (listAdapter == null) {
+ listAdapter = new NewEpisodesListAdapter(activity.get(), itemAccess, new DefaultActionButtonCallback(activity.get()));
+ listView.setAdapter(listAdapter);
+ listView.setEmptyView(txtvEmpty);
+ downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback);
+ downloadObserver.onResume();
+ }
+ if (feedItemDialog != null) {
+ feedItemDialog.updateContent(queueAccess, unreadItems, recentItems);
+ } else if (feedItemDialogSavedInstance != null) {
+ feedItemDialog = FeedItemDialog.newInstance(activity.get(), feedItemDialogSavedInstance);
+ }
+ listAdapter.notifyDataSetChanged();
+ getActivity().supportInvalidateOptionsMenu();
+ updateProgressBarVisibility();
+ updateShowOnlyEpisodesListViewState();
+ }
+
+ private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() {
+ @Override
+ public void onContentChanged() {
+ if (listAdapter != null) {
+ listAdapter.notifyDataSetChanged();
+ }
+ if (feedItemDialog != null && feedItemDialog.isShowing()) {
+ feedItemDialog.updateMenuAppearance();
+ }
+ }
+
+ @Override
+ public void onDownloadDataAvailable(List<Downloader> downloaderList) {
+ NewEpisodesFragment.this.downloaderList = downloaderList;
+ if (listAdapter != null) {
+ listAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ private NewEpisodesListAdapter.ItemAccess itemAccess = new NewEpisodesListAdapter.ItemAccess() {
+
+ @Override
+ public int getCount() {
+ if (itemsLoaded) {
+ return (showOnlyNewEpisodes) ? unreadItems.size() : recentItems.size();
+ }
+ return 0;
+ }
+
+ @Override
+ public FeedItem getItem(int position) {
+ if (itemsLoaded) {
+ return (showOnlyNewEpisodes) ? unreadItems.get(position) : recentItems.get(position);
+ }
+ return null;
+ }
+
+ @Override
+ public int getItemDownloadProgressPercent(FeedItem item) {
+ if (downloaderList != null) {
+ for (Downloader downloader : downloaderList) {
+ if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) {
+ return downloader.getDownloadRequest().getProgressPercent();
+ }
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean isInQueue(FeedItem item) {
+ if (itemsLoaded) {
+ return queueAccess.contains(item.getId());
+ } else {
+ return false;
+ }
+ }
+
+
+ };
+
+ private void updateProgressBarVisibility() {
+ if (!viewsCreated) {
+ return;
+ }
+ ((ActionBarActivity) getActivity())
+ .setSupportProgressBarIndeterminateVisibility(DownloadService.isRunning
+ && DownloadRequester.getInstance().isDownloadingFeeds());
+ }
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EVENTS) != 0) {
+ startItemLoader();
+ updateProgressBarVisibility();
+ }
+ }
+ };
+
+ private void updateShowOnlyEpisodes() {
+ SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ showOnlyNewEpisodes = prefs.getBoolean(PREF_EPISODE_FILTER_BOOL, false);
+ }
+
+ private void setShowOnlyNewEpisodes(boolean newVal) {
+ showOnlyNewEpisodes = newVal;
+ SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_EPISODE_FILTER_BOOL, showOnlyNewEpisodes);
+ editor.commit();
+ if (itemsLoaded && viewsCreated) {
+ listAdapter.notifyDataSetChanged();
+ activity.get().supportInvalidateOptionsMenu();
+ updateShowOnlyEpisodesListViewState();
+ }
+ }
+
+ private void updateShowOnlyEpisodesListViewState() {
+ if (showOnlyNewEpisodes) {
+ listView.setEmptyView(null);
+ txtvEmpty.setVisibility(View.GONE);
+ } else {
+ listView.setEmptyView(txtvEmpty);
+ }
+ }
+
+ private ItemLoader itemLoader;
+
+ private void startItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ itemLoader = new ItemLoader();
+ itemLoader.execute();
+ }
+
+ private void stopItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ }
+
+ private class ItemLoader extends AsyncTask<Void, Void, Object[]> {
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ if (viewsCreated && !itemsLoaded) {
+ listView.setVisibility(View.GONE);
+ txtvEmpty.setVisibility(View.GONE);
+ progLoading.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected Object[] doInBackground(Void... params) {
+ Context context = activity.get();
+ if (context != null) {
+ return new Object[]{DBReader.getUnreadItemsList(context),
+ DBReader.getRecentlyPublishedEpisodes(context, RECENT_EPISODES_LIMIT),
+ QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Object[] lists) {
+ super.onPostExecute(lists);
+ listView.setVisibility(View.VISIBLE);
+ progLoading.setVisibility(View.GONE);
+
+ if (lists != null) {
+ unreadItems = (List<FeedItem>) lists[0];
+ recentItems = (List<FeedItem>) lists[1];
+ queueAccess = (QueueAccess) lists[2];
+ itemsLoaded = true;
+ if (viewsCreated && activity.get() != null) {
+ onFragmentLoaded();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
new file mode 100644
index 000000000..4a07ce2b7
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
@@ -0,0 +1,288 @@
+package de.danoeh.antennapod.fragment;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.ListFragment;
+import android.support.v4.view.MenuItemCompat;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.DefaultActionButtonCallback;
+import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
+import de.danoeh.antennapod.core.asynctask.DownloadObserver;
+import de.danoeh.antennapod.dialog.FeedItemDialog;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class PlaybackHistoryFragment extends ListFragment {
+ private static final String TAG = "PlaybackHistoryFragment";
+
+ private List<FeedItem> playbackHistory;
+ private QueueAccess queue;
+ private FeedItemlistAdapter adapter;
+
+ private boolean itemsLoaded = false;
+ private boolean viewsCreated = false;
+
+ private AtomicReference<Activity> activity = new AtomicReference<Activity>();
+
+ private DownloadObserver downloadObserver;
+ private List<Downloader> downloaderList;
+
+ private FeedItemDialog feedItemDialog;
+ private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ startItemLoader();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ stopItemLoader();
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ stopItemLoader();
+ activity.set(null);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ this.activity.set(activity);
+ if (downloadObserver != null) {
+ downloadObserver.setActivity(activity);
+ downloadObserver.onResume();
+ }
+ if (viewsCreated && itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ adapter = null;
+ viewsCreated = false;
+ if (downloadObserver != null) {
+ downloadObserver.onPause();
+ }
+ if (feedItemDialog != null) {
+ feedItemDialogSavedInstance = feedItemDialog.save();
+ }
+ feedItemDialog = null;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ viewsCreated = true;
+ if (itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+ FeedItem item = adapter.getItem(position - l.getHeaderViewsCount());
+ if (item != null) {
+ feedItemDialog = FeedItemDialog.newInstance(activity.get(), item, queue);
+ feedItemDialog.show();
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ MenuItem clearHistory = menu.add(Menu.NONE, R.id.clear_history_item, Menu.CATEGORY_CONTAINER, R.string.clear_history_label);
+ MenuItemCompat.setShowAsAction(clearHistory, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+ TypedArray drawables = getActivity().obtainStyledAttributes(new int[]{R.attr.content_discard});
+ clearHistory.setIcon(drawables.getDrawable(0));
+ drawables.recycle();
+ }
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ menu.findItem(R.id.clear_history_item).setVisible(playbackHistory != null && !playbackHistory.isEmpty());
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (!super.onOptionsItemSelected(item)) {
+ switch(item.getItemId()) {
+ case R.id.clear_history_item:
+ DBWriter.clearPlaybackHistory(getActivity());
+ return true;
+ default:
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EventDistributor.PLAYBACK_HISTORY_UPDATE) != 0) {
+ startItemLoader();
+ getActivity().supportInvalidateOptionsMenu();
+ }
+ }
+ };
+
+ private void onFragmentLoaded() {
+ if (adapter == null) {
+ adapter = new FeedItemlistAdapter(getActivity(), itemAccess, new DefaultActionButtonCallback(activity.get()), true);
+ setListAdapter(adapter);
+ downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback);
+ downloadObserver.onResume();
+ }
+ setListShown(true);
+ adapter.notifyDataSetChanged();
+ if (feedItemDialog != null && feedItemDialog.isShowing()) {
+ feedItemDialog.updateContent(queue, playbackHistory);
+ } else if (feedItemDialogSavedInstance != null) {
+ feedItemDialog = FeedItemDialog.newInstance(activity.get(), feedItemDialogSavedInstance);
+ }
+ getActivity().supportInvalidateOptionsMenu();
+ }
+
+ private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() {
+ @Override
+ public void onContentChanged() {
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ }
+ if (feedItemDialog != null && feedItemDialog.isShowing()) {
+ feedItemDialog.updateMenuAppearance();
+ }
+ }
+
+ @Override
+ public void onDownloadDataAvailable(List<Downloader> downloaderList) {
+ PlaybackHistoryFragment.this.downloaderList = downloaderList;
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ private FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() {
+ @Override
+ public boolean isInQueue(FeedItem item) {
+ return (queue != null) ? queue.contains(item.getId()) : false;
+ }
+
+ @Override
+ public int getItemDownloadProgressPercent(FeedItem item) {
+ if (downloaderList != null) {
+ for (Downloader downloader : downloaderList) {
+ if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) {
+ return downloader.getDownloadRequest().getProgressPercent();
+ }
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public int getCount() {
+ return (playbackHistory != null) ? playbackHistory.size() : 0;
+ }
+
+ @Override
+ public FeedItem getItem(int position) {
+ return (playbackHistory != null) ? playbackHistory.get(position) : null;
+ }
+ };
+
+ private ItemLoader itemLoader;
+
+ private void startItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ itemLoader = new ItemLoader();
+ itemLoader.execute();
+ }
+
+ private void stopItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ }
+
+ private class ItemLoader extends AsyncTask<Void, Void, Object[]> {
+
+ @Override
+ protected Object[] doInBackground(Void... params) {
+ Context context = activity.get();
+ if (context != null) {
+ List<FeedItem> ph = DBReader.getPlaybackHistory(context);
+ DBReader.loadFeedDataOfFeedItemlist(context, ph);
+ return new Object[]{ph,
+ QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Object[] res) {
+ super.onPostExecute(res);
+ if (res != null) {
+ playbackHistory = (List<FeedItem>) res[0];
+ queue = (QueueAccess) res[1];
+ itemsLoaded = true;
+ if (viewsCreated) {
+ onFragmentLoaded();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
new file mode 100644
index 000000000..3192a84de
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
@@ -0,0 +1,383 @@
+package de.danoeh.antennapod.fragment;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.SearchView;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.mobeta.android.dslv.DragSortListView;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.DefaultActionButtonCallback;
+import de.danoeh.antennapod.adapter.QueueListAdapter;
+import de.danoeh.antennapod.core.asynctask.DownloadObserver;
+import de.danoeh.antennapod.dialog.FeedItemDialog;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+/**
+ * Shows all items in the queue
+ */
+public class QueueFragment extends Fragment {
+ private static final String TAG = "QueueFragment";
+ private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED |
+ EventDistributor.DOWNLOAD_QUEUED |
+ EventDistributor.QUEUE_UPDATE;
+
+ private DragSortListView listView;
+ private QueueListAdapter listAdapter;
+ private TextView txtvEmpty;
+ private ProgressBar progLoading;
+
+ private List<FeedItem> queue;
+ private List<Downloader> downloaderList;
+
+ private boolean itemsLoaded = false;
+ private boolean viewsCreated = false;
+
+ private AtomicReference<Activity> activity = new AtomicReference<Activity>();
+
+ private DownloadObserver downloadObserver = null;
+
+ private FeedItemDialog feedItemDialog;
+ private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance;
+
+ /**
+ * Download observer updates won't result in an upate of the list adapter if this is true.
+ */
+ private boolean blockDownloadObserverUpdate = false;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ startItemLoader();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ this.activity.set((MainActivity) getActivity());
+ if (downloadObserver != null) {
+ downloadObserver.setActivity(getActivity());
+ downloadObserver.onResume();
+ }
+ if (viewsCreated && itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ stopItemLoader();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ this.activity.set((MainActivity) activity);
+ }
+
+ private void resetViewState() {
+ unregisterForContextMenu(listView);
+ listAdapter = null;
+ activity.set(null);
+ viewsCreated = false;
+ blockDownloadObserverUpdate = false;
+ if (downloadObserver != null) {
+ downloadObserver.onPause();
+ }
+ if (feedItemDialog != null) {
+ feedItemDialogSavedInstance = feedItemDialog.save();
+ }
+ feedItemDialog = null;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ resetViewState();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ final SearchView sv = new SearchView(getActivity());
+ MenuItemUtils.addSearchItem(menu, sv);
+ sv.setQueryHint(getString(R.string.search_hint));
+ sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ sv.clearFocus();
+ ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s));
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ return false;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ FeedItem item = itemAccess.getItem(adapterInfo.position);
+
+ MenuInflater inflater = getActivity().getMenuInflater();
+ inflater.inflate(R.menu.queue_context, menu);
+
+ if (item != null) {
+ menu.setHeaderTitle(item.getTitle());
+ }
+
+ menu.findItem(R.id.move_to_top_item).setEnabled(!queue.isEmpty() && queue.get(0) != item);
+ menu.findItem(R.id.move_to_bottom_item).setEnabled(!queue.isEmpty() && queue.get(queue.size() - 1) != item);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ FeedItem selectedItem = itemAccess.getItem(menuInfo.position);
+
+ if (selectedItem == null) {
+ Log.i(TAG, "Selected item at position " + menuInfo.position + " was null, ignoring selection");
+ return super.onContextItemSelected(item);
+ }
+
+ switch (item.getItemId()) {
+ case R.id.move_to_top_item:
+ DBWriter.moveQueueItemToTop(getActivity(), selectedItem.getId(), true);
+ return true;
+ case R.id.move_to_bottom_item:
+ DBWriter.moveQueueItemToBottom(getActivity(), selectedItem.getId(), true);
+ return true;
+ case R.id.remove_from_queue_item:
+ DBWriter.removeQueueItem(getActivity(), selectedItem.getId(), false);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.queue_label);
+
+ View root = inflater.inflate(R.layout.queue_fragment, container, false);
+ listView = (DragSortListView) root.findViewById(android.R.id.list);
+ txtvEmpty = (TextView) root.findViewById(android.R.id.empty);
+ progLoading = (ProgressBar) root.findViewById(R.id.progLoading);
+ listView.setEmptyView(txtvEmpty);
+
+ listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ FeedItem item = (FeedItem) listAdapter.getItem(position - listView.getHeaderViewsCount());
+ if (item != null) {
+ feedItemDialog = FeedItemDialog.newInstance(activity.get(), item, QueueAccess.ItemListAccess(queue));
+ feedItemDialog.show();
+ }
+ }
+ });
+
+ listView.setDragSortListener(new DragSortListView.DragSortListener() {
+ @Override
+ public void drag(int from, int to) {
+ Log.d(TAG, "drag");
+ blockDownloadObserverUpdate = true;
+ }
+
+ @Override
+ public void drop(int from, int to) {
+ Log.d(TAG, "drop");
+ blockDownloadObserverUpdate = false;
+ stopItemLoader();
+ final FeedItem item = queue.remove(from);
+ queue.add(to, item);
+ listAdapter.notifyDataSetChanged();
+ DBWriter.moveQueueItem(getActivity(), from, to, true);
+ }
+
+ @Override
+ public void remove(int which) {
+ }
+ });
+
+ registerForContextMenu(listView);
+
+ if (!itemsLoaded) {
+ progLoading.setVisibility(View.VISIBLE);
+ txtvEmpty.setVisibility(View.GONE);
+ }
+
+ viewsCreated = true;
+
+ if (itemsLoaded && activity.get() != null) {
+ onFragmentLoaded();
+ }
+
+ return root;
+ }
+
+ private void onFragmentLoaded() {
+ if (listAdapter == null) {
+ listAdapter = new QueueListAdapter(activity.get(), itemAccess, new DefaultActionButtonCallback(activity.get()));
+ listView.setAdapter(listAdapter);
+ downloadObserver = new DownloadObserver(activity.get(), new Handler(), downloadObserverCallback);
+ downloadObserver.onResume();
+ }
+ listAdapter.notifyDataSetChanged();
+ if (feedItemDialog != null) {
+ feedItemDialog.updateContent(QueueAccess.ItemListAccess(queue), queue);
+ } else if (feedItemDialogSavedInstance != null) {
+ feedItemDialog = FeedItemDialog.newInstance(activity.get(), feedItemDialogSavedInstance);
+ }
+ }
+
+ private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() {
+ @Override
+ public void onContentChanged() {
+ if (listAdapter != null && !blockDownloadObserverUpdate) {
+ listAdapter.notifyDataSetChanged();
+ }
+ if (feedItemDialog != null && feedItemDialog.isShowing()) {
+ feedItemDialog.updateMenuAppearance();
+ }
+ }
+
+ @Override
+ public void onDownloadDataAvailable(List<Downloader> downloaderList) {
+ QueueFragment.this.downloaderList = downloaderList;
+ if (listAdapter != null && !blockDownloadObserverUpdate) {
+ listAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ private QueueListAdapter.ItemAccess itemAccess = new QueueListAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ return (itemsLoaded) ? queue.size() : 0;
+ }
+
+ @Override
+ public FeedItem getItem(int position) {
+ return (itemsLoaded) ? queue.get(position) : null;
+ }
+
+ @Override
+ public int getItemDownloadProgressPercent(FeedItem item) {
+ if (downloaderList != null) {
+ for (Downloader downloader : downloaderList) {
+ if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) {
+ return downloader.getDownloadRequest().getProgressPercent();
+ }
+ }
+ }
+ return 0;
+ }
+ };
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EVENTS) != 0) {
+ startItemLoader();
+ }
+ }
+ };
+
+ private ItemLoader itemLoader;
+
+ private void startItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ itemLoader = new ItemLoader();
+ itemLoader.execute();
+ }
+
+ private void stopItemLoader() {
+ if (itemLoader != null) {
+ itemLoader.cancel(true);
+ }
+ }
+
+ private class ItemLoader extends AsyncTask<Void, Void, List<FeedItem>> {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ if (viewsCreated && !itemsLoaded) {
+ listView.setVisibility(View.GONE);
+ txtvEmpty.setVisibility(View.GONE);
+ progLoading.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(List<FeedItem> feedItems) {
+ super.onPostExecute(feedItems);
+ listView.setVisibility(View.VISIBLE);
+ progLoading.setVisibility(View.GONE);
+
+ if (feedItems != null) {
+ queue = feedItems;
+ itemsLoaded = true;
+ if (viewsCreated && activity.get() != null) {
+ onFragmentLoaded();
+ }
+ }
+ }
+
+ @Override
+ protected List<FeedItem> doInBackground(Void... params) {
+ Context context = activity.get();
+ if (context != null) {
+ return DBReader.getQueue(context);
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java
new file mode 100644
index 000000000..514b05efd
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.fragment;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.ListFragment;
+import android.view.View;
+import de.danoeh.antennapod.adapter.DownloadlistAdapter;
+import de.danoeh.antennapod.core.asynctask.DownloadObserver;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+
+import java.util.List;
+
+/**
+ * Displays all running downloads and provides actions to cancel them
+ */
+public class RunningDownloadsFragment extends ListFragment {
+ private static final String TAG = "RunningDownloadsFragment";
+
+ private DownloadObserver downloadObserver;
+ private List<Downloader> downloaderList;
+
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (downloadObserver != null) {
+ downloadObserver.onPause();
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ final DownloadlistAdapter downloadlistAdapter = new DownloadlistAdapter(getActivity(), itemAccess);
+ setListAdapter(downloadlistAdapter);
+
+ downloadObserver = new DownloadObserver(getActivity(), new Handler(), new DownloadObserver.Callback() {
+ @Override
+ public void onContentChanged() {
+ downloadlistAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onDownloadDataAvailable(List<Downloader> downloaderList) {
+ RunningDownloadsFragment.this.downloaderList = downloaderList;
+ downloadlistAdapter.notifyDataSetChanged();
+ }
+ });
+ downloadObserver.onResume();
+ }
+
+ private DownloadlistAdapter.ItemAccess itemAccess = new DownloadlistAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ return (downloaderList != null) ? downloaderList.size() : 0;
+ }
+
+ @Override
+ public Downloader getItem(int position) {
+ return (downloaderList != null) ? downloaderList.get(position) : null;
+ }
+
+ @Override
+ public void onSecondaryActionClick(Downloader downloader) {
+ DownloadRequester.getInstance().cancelDownload(getActivity(), downloader.getDownloadRequest().getSource());
+ }
+ };
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
new file mode 100644
index 000000000..7419b42ab
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
@@ -0,0 +1,258 @@
+package de.danoeh.antennapod.fragment;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.ActionBarActivity;
+import android.support.v7.widget.SearchView;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.SearchlistAdapter;
+import de.danoeh.antennapod.dialog.FeedItemDialog;
+import de.danoeh.antennapod.core.feed.*;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.FeedSearcher;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+import java.util.List;
+
+/**
+ * Performs a search operation on all feeds or one specific feed and displays the search result.
+ */
+public class SearchFragment extends ListFragment {
+ private static final String TAG = "SearchFragment";
+
+ private static final String ARG_QUERY = "query";
+ private static final String ARG_FEED = "feed";
+
+ private SearchlistAdapter searchAdapter;
+ private List<SearchResult> searchResults;
+
+ private boolean viewCreated = false;
+ private boolean itemsLoaded = false;
+
+ private QueueAccess queue;
+
+ private FeedItemDialog feedItemDialog;
+ private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance;
+
+ /**
+ * Create a new SearchFragment that searches all feeds.
+ */
+ public static SearchFragment newInstance(String query) {
+ if (query == null) query = "";
+ SearchFragment fragment = new SearchFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_QUERY, query);
+ args.putLong(ARG_FEED, 0);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ /**
+ * Create a new SearchFragment that searches one specific feed.
+ */
+ public static SearchFragment newInstance(String query, long feed) {
+ SearchFragment fragment = newInstance(query);
+ fragment.getArguments().putLong(ARG_FEED, feed);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ setHasOptionsMenu(true);
+ startSearchTask();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ stopSearchTask();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ stopSearchTask();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ searchAdapter = null;
+ viewCreated = false;
+ if (feedItemDialog != null) {
+ feedItemDialogSavedInstance = feedItemDialog.save();
+ }
+ feedItemDialog = null;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle(R.string.search_label);
+ viewCreated = true;
+ if (itemsLoaded) {
+ onFragmentLoaded();
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+ SearchResult result = (SearchResult) l.getAdapter().getItem(position);
+ FeedComponent comp = result.getComponent();
+ if (comp.getClass() == Feed.class) {
+ ((MainActivity)getActivity()).loadFeedFragment(comp.getId());
+ } else {
+ if (comp.getClass() == FeedItem.class) {
+ feedItemDialog = FeedItemDialog.newInstance(getActivity(), (FeedItem) comp, queue);
+ feedItemDialog.show();
+ }
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label);
+ MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+ final SearchView sv = new SearchView(getActivity());
+ sv.setQueryHint(getString(R.string.search_hint));
+ sv.setQuery(getArguments().getString(ARG_QUERY), false);
+ sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ getArguments().putString(ARG_QUERY, s);
+ itemsLoaded = false;
+ startSearchTask();
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ return false;
+ }
+ });
+ MenuItemCompat.setActionView(item, sv);
+ }
+ }
+
+ private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & (EventDistributor.DOWNLOAD_QUEUED)) != 0 && feedItemDialog != null) {
+ feedItemDialog.updateMenuAppearance();
+ }
+ if ((arg & (EventDistributor.UNREAD_ITEMS_UPDATE
+ | EventDistributor.DOWNLOAD_HANDLED
+ | EventDistributor.QUEUE_UPDATE)) != 0) {
+ startSearchTask();
+ }
+ }
+ };
+
+ private void onFragmentLoaded() {
+ if (searchAdapter == null) {
+ searchAdapter = new SearchlistAdapter(getActivity(), itemAccess);
+ setListAdapter(searchAdapter);
+ }
+ searchAdapter.notifyDataSetChanged();
+ setListShown(true);
+ if (feedItemDialog != null && feedItemDialog.isShowing()) {
+ feedItemDialog.setQueue(queue);
+ for (SearchResult result : searchResults) {
+ FeedComponent comp = result.getComponent();
+ if (comp.getClass() == FeedItem.class && ((FeedItem) comp).getId() == feedItemDialog.getItem().getId()) {
+ feedItemDialog.setItem((FeedItem) comp);
+ }
+ }
+ feedItemDialog.updateMenuAppearance();
+ } else if (feedItemDialogSavedInstance != null) {
+ feedItemDialog = FeedItemDialog.newInstance(getActivity(), feedItemDialogSavedInstance);
+ }
+ }
+
+ private final SearchlistAdapter.ItemAccess itemAccess = new SearchlistAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ return (searchResults != null) ? searchResults.size() : 0;
+ }
+
+ @Override
+ public SearchResult getItem(int position) {
+ return (searchResults != null) ? searchResults.get(position) : null;
+ }
+ };
+
+ private SearchTask searchTask;
+
+ private void startSearchTask() {
+ if (searchTask != null) {
+ searchTask.cancel(true);
+ }
+ searchTask = new SearchTask();
+ searchTask.execute(getArguments());
+ }
+
+ private void stopSearchTask() {
+ if (searchTask != null) {
+ searchTask.cancel(true);
+ }
+ }
+
+ private class SearchTask extends AsyncTask<Bundle, Void, Object[]> {
+ @Override
+ protected Object[] doInBackground(Bundle... params) {
+ String query = params[0].getString(ARG_QUERY);
+ long feed = params[0].getLong(ARG_FEED);
+ Context context = getActivity();
+ if (context != null) {
+ return new Object[]{FeedSearcher.performSearch(context, query, feed),
+ QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ if (viewCreated && !itemsLoaded) {
+ setListShown(false);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Object[] results) {
+ super.onPostExecute(results);
+ if (results != null) {
+ itemsLoaded = true;
+ searchResults = (List<SearchResult>) results[0];
+ queue = (QueueAccess) results[1];
+ if (viewCreated) {
+ onFragmentLoaded();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
new file mode 100644
index 000000000..ec8f69368
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
@@ -0,0 +1,131 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+
+/**
+ * Main navigation hub for gpodder.net podcast directory
+ */
+public class GpodnetMainFragment extends Fragment {
+
+ private ViewPager pager;
+ private MainActivity activity;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View root = inflater.inflate(R.layout.pager_fragment, container, false);
+ pager = (ViewPager) root.findViewById(R.id.pager);
+ GpodnetPagerAdapter pagerAdapter = new GpodnetPagerAdapter(getChildFragmentManager(), getResources());
+ pager.setAdapter(pagerAdapter);
+ final ActionBar actionBar = activity.getMainActivtyActionBar();
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ ActionBar.TabListener tabListener = new ActionBar.TabListener() {
+ @Override
+ public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+ pager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+
+ @Override
+ public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+ };
+ actionBar.removeAllTabs();
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.gpodnet_taglist_header)
+ .setTabListener(tabListener));
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.gpodnet_toplist_header)
+ .setTabListener(tabListener));
+ actionBar.setTitle(R.string.gpodnet_main_label);
+
+ pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ actionBar.setSelectedNavigationItem(position);
+ }
+ });
+ return root;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ activity.getMainActivtyActionBar().removeAllTabs();
+ activity.getMainActivtyActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ this.activity = (MainActivity) activity;
+ }
+
+ public class GpodnetPagerAdapter extends FragmentPagerAdapter {
+
+
+ private static final int NUM_PAGES = 2;
+ private static final int POS_TAGS = 0;
+ private static final int POS_TOPLIST = 1;
+ private static final int POS_SUGGESTIONS = 2;
+
+ Resources resources;
+
+ public GpodnetPagerAdapter(FragmentManager fm, Resources resources) {
+ super(fm);
+ this.resources = resources;
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+ switch (i) {
+ case POS_TAGS:
+ return new TagListFragment();
+ case POS_TOPLIST:
+ return new PodcastTopListFragment();
+ case POS_SUGGESTIONS:
+ return new SuggestionListFragment();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case POS_TAGS:
+ return getString(R.string.gpodnet_taglist_header);
+ case POS_TOPLIST:
+ return getString(R.string.gpodnet_toplist_header);
+ case POS_SUGGESTIONS:
+ return getString(R.string.gpodnet_suggestions_header);
+ default:
+ return super.getPageTitle(position);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_PAGES;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
new file mode 100644
index 000000000..15a0b55b1
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
@@ -0,0 +1,167 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
+import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+import java.util.List;
+
+/**
+ * Displays a list of GPodnetPodcast-Objects in a GridView
+ */
+public abstract class PodcastListFragment extends Fragment {
+ private static final String TAG = "PodcastListFragment";
+
+ private GridView gridView;
+ private ProgressBar progressBar;
+ private TextView txtvError;
+ private Button butRetry;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ final android.support.v7.widget.SearchView sv = new android.support.v7.widget.SearchView(getActivity());
+ MenuItemUtils.addSearchItem(menu, sv);
+ sv.setQueryHint(getString(R.string.gpodnet_search_hint));
+ sv.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ sv.clearFocus();
+ ((MainActivity) getActivity()).loadChildFragment(SearchListFragment.newInstance(s));
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ return false;
+ }
+ });
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.gpodnet_podcast_list, container, false);
+
+ gridView = (GridView) root.findViewById(R.id.gridView);
+ progressBar = (ProgressBar) root.findViewById(R.id.progressBar);
+ txtvError = (TextView) root.findViewById(R.id.txtvError);
+ butRetry = (Button) root.findViewById(R.id.butRetry);
+
+ gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ onPodcastSelected((GpodnetPodcast) gridView.getAdapter().getItem(position));
+ }
+ });
+ butRetry.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ loadData();
+ }
+ });
+
+ loadData();
+ return root;
+ }
+
+ protected void onPodcastSelected(GpodnetPodcast selection) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Selected podcast: " + selection.toString());
+ Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class);
+ intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, selection.getUrl());
+ intent.putExtra(DefaultOnlineFeedViewActivity.ARG_TITLE, getString(R.string.gpodnet_main_label));
+ startActivity(intent);
+ }
+
+ protected abstract List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException;
+
+ protected final void loadData() {
+ AsyncTask<Void, Void, List<GpodnetPodcast>> loaderTask = new AsyncTask<Void, Void, List<GpodnetPodcast>>() {
+ volatile Exception exception = null;
+
+ @Override
+ protected List<GpodnetPodcast> doInBackground(Void... params) {
+ GpodnetService service = null;
+ try {
+ service = new GpodnetService();
+ return loadPodcastData(service);
+ } catch (GpodnetServiceException e) {
+ exception = e;
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (service != null) {
+ service.shutdown();
+ }
+ }
+ }
+
+ @Override
+ protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) {
+ super.onPostExecute(gpodnetPodcasts);
+ final Context context = getActivity();
+ if (context != null && gpodnetPodcasts != null && gpodnetPodcasts.size() > 0) {
+ PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts);
+ gridView.setAdapter(listAdapter);
+ listAdapter.notifyDataSetChanged();
+
+ progressBar.setVisibility(View.GONE);
+ gridView.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+ butRetry.setVisibility(View.GONE);
+ } else if (context != null && gpodnetPodcasts != null) {
+ gridView.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ txtvError.setText(getString(R.string.search_status_no_results));
+ txtvError.setVisibility(View.VISIBLE);
+ butRetry.setVisibility(View.GONE);
+ } else if (context != null) {
+ gridView.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ butRetry.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ gridView.setVisibility(View.GONE);
+ progressBar.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+ butRetry.setVisibility(View.GONE);
+ }
+ };
+
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ loaderTask.execute();
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java
new file mode 100644
index 000000000..33a35fa90
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
+
+import java.util.List;
+
+/**
+ *
+ */
+public class PodcastTopListFragment extends PodcastListFragment {
+ private static final String TAG = "PodcastTopListFragment";
+ private static final int PODCAST_COUNT = 50;
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ return service.getPodcastToplist(PODCAST_COUNT);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java
new file mode 100644
index 000000000..635842196
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java
@@ -0,0 +1,80 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.os.Bundle;
+import android.support.v7.widget.SearchView;
+import android.view.Menu;
+import android.view.MenuInflater;
+
+import org.apache.commons.lang3.Validate;
+
+import java.util.List;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+/**
+ * Performs a search on the gpodder.net directory and displays the results.
+ */
+public class SearchListFragment extends PodcastListFragment {
+ private static final String ARG_QUERY = "query";
+
+ private String query;
+
+ public static SearchListFragment newInstance(String query) {
+ SearchListFragment fragment = new SearchListFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_QUERY, query);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getArguments() != null && getArguments().containsKey(ARG_QUERY)) {
+ this.query = getArguments().getString(ARG_QUERY);
+ } else {
+ this.query = "";
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ final SearchView sv = new SearchView(getActivity());
+ if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ MenuItemUtils.addSearchItem(menu, sv);
+ sv.setQueryHint(getString(R.string.gpodnet_search_hint));
+ sv.setQuery(query, false);
+ sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ sv.clearFocus();
+ changeQuery(s);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ return false;
+ }
+ });
+ }
+ }
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ return service.searchPodcasts(query, 0);
+ }
+
+ public void changeQuery(String query) {
+ Validate.notNull(query);
+
+ this.query = query;
+ loadData();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java
new file mode 100644
index 000000000..133bb0281
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays suggestions from gpodder.net
+ */
+public class SuggestionListFragment extends PodcastListFragment {
+ private static final int SUGGESTIONS_COUNT = 50;
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ if (GpodnetPreferences.loggedIn()) {
+ service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ return service.getSuggestions(SUGGESTIONS_COUNT);
+ } else {
+ return new ArrayList<GpodnetPodcast>();
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
new file mode 100644
index 000000000..7e02b647f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
@@ -0,0 +1,50 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.os.Bundle;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
+
+import java.util.List;
+
+/**
+ * Shows all podcasts from gpodder.net that belong to a specific tag.
+ * Use the newInstance method of this class to create a new TagFragment.
+ */
+public class TagFragment extends PodcastListFragment {
+
+ private static final String TAG = "TagFragment";
+ private static final int PODCAST_COUNT = 50;
+
+ private GpodnetTag tag;
+
+ public static TagFragment newInstance(String tagName) {
+ Validate.notNull(tagName);
+ TagFragment fragment = new TagFragment();
+ Bundle args = new Bundle();
+ args.putString("tag", tagName);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Bundle args = getArguments();
+ Validate.isTrue(args != null && args.getString("tag") != null, "args invalid");
+
+ tag = new GpodnetTag(args.getString("tag"));
+ ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(tag.getName());
+ }
+
+ @Override
+ protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ return service.getPodcastsForTag(tag, PODCAST_COUNT);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
new file mode 100644
index 000000000..24e0e4caa
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
@@ -0,0 +1,146 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v7.widget.SearchView;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
+import de.danoeh.antennapod.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+
+public class TagListFragment extends ListFragment {
+ private static final String TAG = "TagListFragment";
+ private static final int COUNT = 50;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ if (!MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) {
+ final SearchView sv = new SearchView(getActivity());
+ MenuItemUtils.addSearchItem(menu, sv);
+ sv.setQueryHint(getString(R.string.gpodnet_search_hint));
+ sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ Activity activity = getActivity();
+ if (activity != null) {
+ sv.clearFocus();
+ ((MainActivity) activity).loadChildFragment(SearchListFragment.newInstance(s));
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ return false;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ String selectedTag = (String) getListAdapter().getItem(position);
+ MainActivity activity = (MainActivity) getActivity();
+ activity.loadChildFragment(TagFragment.newInstance(selectedTag));
+ }
+ });
+
+ startLoadTask();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ cancelLoadTask();
+ }
+
+ private AsyncTask<Void, Void, List<GpodnetTag>> loadTask;
+
+ private void cancelLoadTask() {
+ if (loadTask != null && !loadTask.isCancelled()) {
+ loadTask.cancel(true);
+ }
+ }
+
+ private void startLoadTask() {
+ cancelLoadTask();
+ loadTask = new AsyncTask<Void, Void, List<GpodnetTag>>() {
+ private Exception exception;
+
+ @Override
+ protected List<GpodnetTag> doInBackground(Void... params) {
+ GpodnetService service = new GpodnetService();
+ try {
+ return service.getTopTags(COUNT);
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ } finally {
+ service.shutdown();
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ setListShown(false);
+ }
+
+ @Override
+ protected void onPostExecute(List<GpodnetTag> gpodnetTags) {
+ super.onPostExecute(gpodnetTags);
+ final Context context = getActivity();
+ if (context != null) {
+ if (gpodnetTags != null) {
+ List<String> tagNames = new ArrayList<String>();
+ for (GpodnetTag tag : gpodnetTags) {
+ tagNames.add(tag.getName());
+ }
+ setListAdapter(new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, tagNames));
+ } else if (exception != null) {
+ TextView txtvError = new TextView(getActivity());
+ txtvError.setText(exception.getMessage());
+ getListView().setEmptyView(txtvError);
+ }
+ setListShown(true);
+
+ }
+ }
+ };
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ loadTask.execute();
+ }
+ }
+}
+
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
new file mode 100644
index 000000000..8ccbdafc6
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
@@ -0,0 +1,191 @@
+package de.danoeh.antennapod.menuhandler;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.core.util.ShareUtils;
+
+/**
+ * Handles interactions with the FeedItemMenu.
+ */
+public class FeedItemMenuHandler {
+ private static final String TAG = "FeedItemMenuHandler";
+
+ private FeedItemMenuHandler() {
+
+ }
+
+ /**
+ * Used by the MenuHandler to access different types of menus through one
+ * interface
+ */
+ public interface MenuInterface {
+ /**
+ * Implementations of this method should call findItem(id) on their
+ * menu-object and call setVisibility(visibility) on the returned
+ * MenuItem object.
+ */
+ abstract void setItemVisibility(int id, boolean visible);
+ }
+
+ /**
+ * This method should be called in the prepare-methods of menus. It changes
+ * the visibility of the menu items depending on a FeedItem's attributes.
+ *
+ * @param mi An instance of MenuInterface that the method uses to change a
+ * MenuItem's visibility
+ * @param selectedItem The FeedItem for which the menu is supposed to be prepared
+ * @param showExtendedMenu True if MenuItems that let the user share information about
+ * the FeedItem and visit its website should be set visible. This
+ * parameter should be set to false if the menu space is limited.
+ * @param queueAccess Used for testing if the queue contains the selected item
+ * @return Returns true if selectedItem is not null.
+ */
+ public static boolean onPrepareMenu(MenuInterface mi,
+ FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) {
+ 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;
+
+ 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);
+ }
+
+ 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 (!BuildConfig.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 (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) {
+ mi.setItemVisibility(R.id.support_item, false);
+ }
+ return true;
+ }
+
+ /**
+ * The same method as onPrepareMenu(MenuInterface, FeedItem, boolean, QueueAccess), but lets the
+ * caller also specify a list of menu items that should not be shown.
+ *
+ * @param excludeIds Menu item that should be excluded
+ * @return true if selectedItem is not null.
+ */
+ public static boolean onPrepareMenu(MenuInterface mi,
+ FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess, int... excludeIds) {
+ boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess);
+ if (rc && excludeIds != null) {
+ for (int id : excludeIds) {
+ mi.setItemVisibility(id, false);
+ }
+ }
+
+ return rc;
+ }
+
+ 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;
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java
new file mode 100644
index 000000000..62ae28820
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java
@@ -0,0 +1,86 @@
+package de.danoeh.antennapod.menuhandler;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.ShareUtils;
+
+/** Handles interactions with the FeedItemMenu. */
+public class FeedMenuHandler {
+ private static final String TAG = "FeedMenuHandler";
+
+ public static boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
+ inflater.inflate(R.menu.feedlist, menu);
+ return true;
+ }
+
+ public static boolean onPrepareOptionsMenu(Menu menu, Feed selectedFeed) {
+ if (selectedFeed == null) {
+ return true;
+ }
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Preparing options menu");
+ menu.findItem(R.id.mark_all_read_item).setVisible(
+ selectedFeed.hasNewItems(true));
+ 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(
+ selectedFeed)) {
+ refresh.setVisible(false);
+ } else {
+ refresh.setVisible(true);
+ }
+
+ return true;
+ }
+
+ /**
+ * NOTE: This method does not handle clicks on the 'remove feed' - item.
+ *
+ * @throws DownloadRequestException
+ */
+ public static boolean onOptionsItemClicked(Context context, MenuItem item,
+ Feed selectedFeed) throws DownloadRequestException {
+ switch (item.getItemId()) {
+ case R.id.refresh_item:
+ DBTasks.refreshFeed(context, selectedFeed);
+ break;
+ case R.id.mark_all_read_item:
+ DBWriter.markFeedRead(context, selectedFeed.getId());
+ break;
+ case R.id.visit_website_item:
+ Uri uri = Uri.parse(selectedFeed.getLink());
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ break;
+ case R.id.support_item:
+ DBTasks.flattrFeedIfLoggedIn(context, selectedFeed);
+ break;
+ case R.id.share_link_item:
+ ShareUtils.shareFeedlink(context, selectedFeed);
+ break;
+ case R.id.share_source_item:
+ ShareUtils.shareFeedDownloadLink(context, selectedFeed);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java
new file mode 100644
index 000000000..c4a96ac3f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java
@@ -0,0 +1,31 @@
+package de.danoeh.antennapod.menuhandler;
+
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.SearchView;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import de.danoeh.antennapod.core.R;
+
+/**
+ * Utilities for menu items
+ */
+public class MenuItemUtils {
+
+ public static MenuItem addSearchItem(Menu menu, SearchView searchView) {
+ MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label);
+ MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+ MenuItemCompat.setActionView(item, searchView);
+ return item;
+ }
+
+ /**
+ * Checks if the navigation drawer of the DrawerActivity is opened. This can be useful for Fragments
+ * that hide their menu if the navigation drawer is open.
+ *
+ * @return True if the drawer is open, false otherwise (also if the parameter is null)
+ */
+ public static boolean isActivityDrawerOpen(NavDrawerActivity activity) {
+ return activity != null && activity.isDrawerOpen();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java
new file mode 100644
index 000000000..6ceaaada4
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java
@@ -0,0 +1,9 @@
+package de.danoeh.antennapod.menuhandler;
+
+/**
+ * Defines useful methods for activities that have a navigation drawer
+ */
+public interface NavDrawerActivity {
+
+ public boolean isDrawerOpen();
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
new file mode 100644
index 000000000..f55a7603f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+
+import org.apache.commons.lang3.StringUtils;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+
+public class ConnectivityActionReceiver extends BroadcastReceiver {
+ private static final String TAG = "ConnectivityActionReceiver";
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received intent");
+
+ if (NetworkUtils.autodownloadNetworkAvailable(context)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "auto-dl network available, starting auto-download");
+ DBTasks.autodownloadUndownloadedItems(context);
+ } else { // if new network is Wi-Fi, finish ongoing downloads,
+ // otherwise cancel all downloads
+ ConnectivityManager cm = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo ni = cm.getActiveNetworkInfo();
+ if (ni == null || ni.getType() != ConnectivityManager.TYPE_WIFI) {
+ if (BuildConfig.DEBUG)
+ Log.i(TAG,
+ "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads");
+ DownloadRequester.getInstance().cancelAllDownloads(context);
+ }
+
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java
new file mode 100644
index 000000000..7ab386edf
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java
@@ -0,0 +1,49 @@
+package de.danoeh.antennapod.receiver;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.apache.commons.lang3.StringUtils;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.service.PlayerWidgetService;
+
+public class PlayerWidget extends AppWidgetProvider {
+ private static final String TAG = "PlayerWidget";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), PlaybackService.FORCE_WIDGET_UPDATE)) {
+ startUpdate(context);
+ } else if (StringUtils.equals(intent.getAction(), PlaybackService.STOP_WIDGET_UPDATE)) {
+ stopUpdate(context);
+ }
+
+ }
+
+ @Override
+ public void onEnabled(Context context) {
+ super.onEnabled(context);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Widget enabled");
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager,
+ int[] appWidgetIds) {
+ startUpdate(context);
+ }
+
+ private void startUpdate(Context context) {
+ context.startService(new Intent(context, PlayerWidgetService.class));
+ }
+
+ private void stopUpdate(Context context) {
+ context.stopService(new Intent(context, PlayerWidgetService.class));
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java
new file mode 100644
index 000000000..359a546f6
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.widget.Toast;
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * Receives intents from AntennaPod Single Purpose apps
+ */
+public class SPAReceiver extends BroadcastReceiver{
+ private static final String TAG = "SPAReceiver";
+
+ public static final String ACTION_SP_APPS_QUERY_FEEDS = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS";
+ public static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE";
+ public static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA = "feeds";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), ACTION_SP_APPS_QUERY_FEEDS_REPSONSE)) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Received SP_APPS_QUERY_RESPONSE");
+ if (intent.hasExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA)) {
+ String[] feedUrls = intent.getStringArrayExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA);
+ if (feedUrls != null) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Received feeds list: " + Arrays.toString(feedUrls));
+ for (String url : feedUrls) {
+ Feed f = new Feed(url, new Date());
+ try {
+ DownloadRequester.getInstance().downloadFeed(context, f);
+ } catch (DownloadRequestException e) {
+ Log.e(TAG, "Error while trying to add feed " + url);
+ e.printStackTrace();
+ }
+ }
+ Toast.makeText(context, R.string.sp_apps_importing_feeds_msg, Toast.LENGTH_LONG).show();
+
+ } else {
+ Log.e(TAG, "Received invalid SP_APPS_QUERY_REPSONSE: extra was null");
+ }
+ } else {
+ Log.e(TAG, "Received invalid SP_APPS_QUERY_RESPONSE: Contains no extra");
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java
new file mode 100644
index 000000000..4622e2f79
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java
@@ -0,0 +1,203 @@
+package de.danoeh.antennapod.service;
+
+import android.app.PendingIntent;
+import android.app.Service;
+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;
+import android.view.View;
+import android.widget.RemoteViews;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.receiver.PlayerWidget;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+/** Updates the state of the player widget */
+public class PlayerWidgetService extends Service {
+ private static final String TAG = "PlayerWidgetService";
+
+ private PlaybackService playbackService;
+
+ /** Controls write access to playbackservice reference */
+ private Object psLock;
+
+ /** True while service is updating the widget */
+ private volatile boolean isUpdating;
+
+ public PlayerWidgetService() {
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Service created");
+ isUpdating = false;
+ psLock = new Object();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Service is about to be destroyed");
+ try {
+ unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "IllegalArgumentException when trying to unbind service");
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (!isUpdating) {
+ if (playbackService == null && PlaybackService.isRunning) {
+ bindService(new Intent(this, PlaybackService.class),
+ mConnection, 0);
+ } else {
+ startViewUpdaterIfNotRunning();
+ }
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Service was called while updating. Ignoring update request");
+ }
+ return Service.START_NOT_STICKY;
+ }
+
+ private void updateViews() {
+ if (playbackService == null) {
+ return;
+ }
+ isUpdating = true;
+
+ ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
+ AppWidgetManager manager = AppWidgetManager.getInstance(this);
+ RemoteViews views = new RemoteViews(getPackageName(),
+ R.layout.player_widget);
+ PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this), 0);
+
+ views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer);
+ final Playable media = playbackService.getPlayable();
+ if (playbackService != null && media != null) {
+ PlayerStatus status = playbackService.getStatus();
+
+ views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
+
+ if (status == PlayerStatus.PLAYING) {
+ String progressString = getProgressString(playbackService);
+ if (progressString != null) {
+ 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 {
+ views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE);
+ views.setTextViewText(R.id.txtvTitle,
+ this.getString(R.string.no_media_playing_label));
+ views.setImageViewResource(R.id.butPlay, R.drawable.av_play);
+
+ }
+
+ manager.updateAppWidget(playerWidget, views);
+ isUpdating = false;
+ }
+
+ /** Creates an intent which fakes a mediabutton press */
+ private PendingIntent createMediaButtonIntent() {
+ KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ Intent startingIntent = new Intent(
+ MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
+ startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+
+ return PendingIntent.getBroadcast(this, 0, startingIntent, 0);
+ }
+
+ private String getProgressString(PlaybackService ps) {
+ int position = ps.getCurrentPosition();
+ int duration = ps.getDuration();
+ if (position != PlaybackService.INVALID_TIME
+ && duration != PlaybackService.INVALID_TIME) {
+ return Converter.getDurationStringLong(position) + " / "
+ + Converter.getDurationStringLong(duration);
+ } else {
+ return null;
+ }
+ }
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Connection to service established");
+ synchronized (psLock) {
+ playbackService = ((PlaybackService.LocalBinder) service)
+ .getService();
+ startViewUpdaterIfNotRunning();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (psLock) {
+ playbackService = null;
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Disconnected from service");
+ }
+ }
+
+ };
+
+ private void startViewUpdaterIfNotRunning() {
+ if (!isUpdating) {
+ ViewUpdater updateThread = new ViewUpdater(this);
+ updateThread.start();
+ }
+ }
+
+ class ViewUpdater extends Thread {
+ private static final String THREAD_NAME = "ViewUpdater";
+ private PlayerWidgetService service;
+
+ public ViewUpdater(PlayerWidgetService service) {
+ super();
+ setName(THREAD_NAME);
+ this.service = service;
+
+ }
+
+ @Override
+ public void run() {
+ synchronized (psLock) {
+ service.updateViews();
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java
new file mode 100644
index 000000000..75cbd8b5a
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.spa;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.receiver.SPAReceiver;
+
+/**
+ * Provides methods related to AntennaPodSP (https://github.com/danieloeh/AntennaPodSP)
+ */
+public class SPAUtil {
+ private static final String TAG = "SPAUtil";
+
+ private static final String PREF_HAS_QUERIED_SP_APPS = "prefSPAUtil.hasQueriedSPApps";
+
+ private SPAUtil() {
+ }
+
+
+ /**
+ * Sends an ACTION_SP_APPS_QUERY_FEEDS intent to all AntennaPod Single Purpose apps.
+ * The receiving single purpose apps will then send their feeds back to AntennaPod via an
+ * ACTION_SP_APPS_QUERY_FEEDS_RESPONSE intent.
+ * This intent will only be sent once.
+ *
+ * @return True if an intent was sent, false otherwise (for example if the intent has already been
+ * sent before.
+ */
+ public static synchronized boolean sendSPAppsQueryFeedsIntent(Context context) {
+ if (context == null) throw new IllegalArgumentException("context = null");
+ final Context appContext = context.getApplicationContext();
+ if (appContext == null) {
+ Log.wtf(TAG, "Unable to get application context");
+ return false;
+ }
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
+ if (!prefs.getBoolean(PREF_HAS_QUERIED_SP_APPS, false)) {
+ appContext.sendBroadcast(new Intent(SPAReceiver.ACTION_SP_APPS_QUERY_FEEDS));
+ if (BuildConfig.DEBUG) Log.d(TAG, "Sending SP_APPS_QUERY_FEEDS intent");
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, true);
+ editor.commit();
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Resets all preferences created by this class. Should only be used for debug purposes.
+ */
+ public static void resetSPAPreferences(Context c) {
+ if (BuildConfig.DEBUG) {
+ Validate.notNull(c);
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(c.getApplicationContext()).edit();
+ editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, false);
+ editor.commit();
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java b/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java
new file mode 100644
index 000000000..f930c912a
--- /dev/null
+++ b/app/src/main/java/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/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml
new file mode 100644
index 000000000..d3567dc31
--- /dev/null
+++ b/app/src/main/res/anim/fade_in.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <alpha
+ android:duration="500"
+ android:fromAlpha="0.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:toAlpha="1.0" />
+</set> \ No newline at end of file
diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml
new file mode 100644
index 000000000..ddf12d13f
--- /dev/null
+++ b/app/src/main/res/anim/fade_out.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <alpha
+ android:duration="500"
+ android:fromAlpha="1.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:toAlpha="0.0" />
+
+</set> \ No newline at end of file
diff --git a/app/src/main/res/layout-land/audioplayer_activity.xml b/app/src/main/res/layout-land/audioplayer_activity.xml
new file mode 100644
index 000000000..8f8fdbee3
--- /dev/null
+++ b/app/src/main/res/layout-land/audioplayer_activity.xml
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.DrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <FrameLayout
+ android:id="@+id/contentView"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.5">
+ </FrameLayout>
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0.5"
+ android:background="?attr/non_transparent_background"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/navBar"
+ android:layout_width="fill_parent"
+ android:layout_height="60dp"
+ android:layout_alignParentTop="true">
+
+ <ImageButton
+ android:id="@+id/butNavLeft"
+ android:contentDescription="@string/show_shownotes_label"
+ android:layout_width="60dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:background="?attr/borderless_button"
+ android:padding="4dp"/>
+
+ <ImageButton
+ android:id="@+id/butNavRight"
+ android:contentDescription="@string/show_chapters_label"
+ android:layout_width="60dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:background="?attr/borderless_button"
+ android:padding="4dp"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_toLeftOf="@id/butNavRight"
+ android:layout_toRightOf="@id/butNavLeft"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"/>
+
+ <TextView
+ android:id="@+id/txtvFeed"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvTitle"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_toLeftOf="@id/butNavRight"
+ android:layout_toRightOf="@id/butNavLeft"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_small"/>
+ </RelativeLayout>
+
+ <View
+ android:id="@+id/navBarDivider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/navBar"
+ android:background="@color/bright_blue"/>
+
+ <RelativeLayout
+ android:id="@+id/player_control"
+ android:layout_width="match_parent"
+ android:layout_height="80dp"
+ android:layout_alignParentBottom="true"
+ android:background="?attr/overlay_background">
+
+ <ImageButton
+ android:id="@+id/butPlay"
+ android:contentDescription="@string/pause_label"
+ android:layout_width="80dp"
+ android:layout_height="match_parent"
+ android:layout_centerHorizontal="true"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_pause"/>
+
+ <ImageButton
+ android:id="@+id/butRev"
+ android:contentDescription="@string/rewind_label"
+ android:layout_width="60dp"
+ android:layout_height="match_parent"
+ android:layout_toLeftOf="@id/butPlay"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_rewind"/>
+
+ <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"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_fast_forward"/>
+
+ <Button
+ android:id="@+id/butPlaybackSpeed"
+ android:layout_width="60dp"
+ android:layout_height="match_parent"
+ android:layout_toRightOf="@id/butFF"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_fast_forward"
+ android:textColor="@color/gray"
+ android:textSize="@dimen/text_size_medium"
+ android:visibility="gone"/>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/playtime_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@id/player_control"
+ android:layout_alignParentLeft="true"
+ android:background="?attr/overlay_drawable">
+
+ <TextView
+ android:id="@+id/txtvPosition"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="16dp"
+ android:text="@string/position_default_label"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <TextView
+ android:id="@+id/txtvLength"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="16dp"
+ android:text="@string/position_default_label"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <SeekBar
+ android:id="@+id/sbPosition"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_toLeftOf="@id/txtvLength"
+ android:layout_toRightOf="@id/txtvPosition"
+ android:max="500"/>
+ </RelativeLayout>
+ </RelativeLayout>
+
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/nav_list"
+ android:layout_width="@dimen/drawer_width"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:choiceMode="singleChoice"
+ android:background="?attr/nav_drawer_background"
+ android:scrollbarStyle="outsideOverlay"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"/>
+
+</android.support.v4.widget.DrawerLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-land/videoplayer_activity.xml b/app/src/main/res/layout-land/videoplayer_activity.xml
new file mode 100644
index 000000000..f1e54f7c3
--- /dev/null
+++ b/app/src/main/res/layout-land/videoplayer_activity.xml
@@ -0,0 +1,84 @@
+<?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">
+
+ <de.danoeh.antennapod.view.AspectRatioVideoView
+ android:id="@+id/videoview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"/>
+
+ <ProgressBar
+ android:id="@+id/progressIndicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="invisible"
+ 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="#80000000"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/timecontrol"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:paddingTop="8dp"
+ android:layout_marginBottom="4dp">
+
+ <TextView
+ android:id="@+id/txtvPosition"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="4dp"
+ android:textColor="@color/white"
+ android:textStyle="bold"
+ android:text="@string/position_default_label"/>
+
+ <TextView
+ android:id="@+id/txtvLength"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="4dp"
+ android:textColor="@color/white"
+ android:textStyle="bold"
+ android:text="@string/position_default_label"/>
+
+ <SeekBar
+ android:id="@+id/sbPosition"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@+id/txtvLength"
+ android:layout_toRightOf="@+id/txtvPosition"
+ android:max="500"/>
+ </RelativeLayout>
+ </LinearLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-v14/authentication_dialog.xml b/app/src/main/res/layout-v14/authentication_dialog.xml
new file mode 100644
index 000000000..ed05dab1c
--- /dev/null
+++ b/app/src/main/res/layout-v14/authentication_dialog.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <EditText
+ android:id="@+id/etxtUsername"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_margin="16dp"
+ android:hint="@string/username_label"/>
+
+ <EditText
+ android:id="@+id/etxtPassword"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_margin="16dp"
+ android:inputType="textPassword"
+ android:hint="@string/password_label"/>
+
+ <CheckBox
+ android:id="@+id/chkSaveUsernamePassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/save_username_password_label"/>
+ </LinearLayout>
+
+ <RelativeLayout
+ android:id="@+id/footer"
+ android:layout_width="fill_parent"
+ android:layout_height="48dp" >
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_alignParentTop="true"
+ android:background="?android:attr/dividerVertical" />
+
+ <View
+ android:id="@+id/horizontal_divider"
+ android:layout_width="1dip"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="4dp"
+ android:layout_marginTop="4dp"
+ android:background="?android:attr/dividerVertical" />
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/cancel_label" />
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/confirm_label" />
+ </RelativeLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-v14/directory_chooser.xml b/app/src/main/res/layout-v14/directory_chooser.xml
new file mode 100644
index 000000000..f0bef72e4
--- /dev/null
+++ b/app/src/main/res/layout-v14/directory_chooser.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ android:id="@+id/footer"
+ android:layout_width="fill_parent"
+ android:layout_height="48dp"
+ android:layout_alignParentBottom="true" >
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_alignParentTop="true"
+ android:background="?android:attr/dividerVertical" />
+
+ <View
+ android:id="@+id/horizontal_divider"
+ android:layout_width="1dip"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="4dp"
+ android:layout_marginTop="4dp"
+ android:background="?android:attr/dividerVertical" />
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/cancel_label" />
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/confirm_label" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/directory_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true" >
+
+ <ImageButton
+ android:id="@+id/butNavUp"
+ android:contentDescription="@string/navigate_upwards_label"
+ android:layout_width="60dp"
+ android:layout_height="60dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="?attr/borderless_button"
+ android:src="?attr/navigation_up" />
+
+ <TextView
+ android:id="@+id/txtvSelectedFolderLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_toRightOf="@id/butNavUp"
+ android:text="@string/selected_folder_label"
+ android:textStyle="bold" >
+ </TextView>
+
+ <TextView
+ android:id="@+id/txtvSelectedFolder"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_below="@id/txtvSelectedFolderLabel"
+ android:layout_margin="8dp"
+ android:layout_toRightOf="@id/butNavUp"
+ android:ellipsize="start"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/butNavUp"
+ android:background="@color/bright_blue" />
+ </RelativeLayout>
+
+ <ListView
+ android:id="@+id/directory_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_above="@id/footer"
+ android:layout_below="@id/directory_info" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-v14/download_authentication_activity.xml b/app/src/main/res/layout-v14/download_authentication_activity.xml
new file mode 100644
index 000000000..c1fe55ceb
--- /dev/null
+++ b/app/src/main/res/layout-v14/download_authentication_activity.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/authentication_notification_title"
+ android:layout_alignParentTop="true"
+ android:textSize="@dimen/text_size_large"
+ android:layout_margin="16dp"
+ android:textColor="@color/bright_blue"
+ android:textStyle="italic"/>
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/authentication_notification_msg"
+ android:layout_below="@id/txtvTitle"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="16dp"/>
+
+ <EditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:id="@+id/etxtUsername"
+ android:hint="@string/username_label"
+ android:layout_below="@id/txtvDescription"/>
+
+ <EditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:id="@+id/etxtPassword"
+ android:hint="@string/password_label"
+ android:inputType="textPassword"
+ android:layout_below="@id/etxtUsername"/>
+
+ <RelativeLayout
+ android:id="@+id/footer"
+ android:layout_width="fill_parent"
+ android:layout_height="48dp"
+ android:focusableInTouchMode="true"
+ android:layout_alignParentBottom="true">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_alignParentTop="true"
+ android:background="?android:attr/dividerVertical"/>
+
+ <View
+ android:id="@+id/horizontal_divider"
+ android:layout_width="1dip"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="4dp"
+ android:layout_marginTop="4dp"
+ android:background="?android:attr/dividerVertical"/>
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/cancel_label"/>
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/confirm_label"/>
+ </RelativeLayout>
+
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-v14/opml_selection.xml b/app/src/main/res/layout-v14/opml_selection.xml
new file mode 100644
index 000000000..3133debd1
--- /dev/null
+++ b/app/src/main/res/layout-v14/opml_selection.xml
@@ -0,0 +1,61 @@
+<?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="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ android:id="@+id/footer"
+ android:layout_width="fill_parent"
+ android:layout_height="48dp"
+ android:layout_alignParentBottom="true" >
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_alignParentTop="true"
+ android:background="?android:attr/dividerVertical" />
+
+ <View
+ android:id="@+id/horizontal_divider"
+ android:layout_width="1dip"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="4dp"
+ android:layout_marginTop="4dp"
+ android:background="?android:attr/dividerVertical" />
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/cancel_label" />
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/confirm_label" />
+ </RelativeLayout>
+
+ <ListView
+ android:id="@+id/feedlist"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_above="@id/footer"
+ android:layout_alignParentTop="true"
+ tools:listitem="@android:layout/simple_list_item_multiple_choice" >
+ </ListView>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-v14/time_dialog.xml b/app/src/main/res/layout-v14/time_dialog.xml
new file mode 100644
index 000000000..7fd4309d5
--- /dev/null
+++ b/app/src/main/res/layout-v14/time_dialog.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <EditText
+ android:id="@+id/etxtTime"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="8dp"
+ android:ems="7"
+ android:hint="@string/enter_time_here_label"
+ android:inputType="number"
+ android:maxLength="2" >
+
+ <requestFocus />
+ </EditText>
+
+ <Spinner
+ android:id="@+id/spTimeUnit"
+ android:layout_width="180dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp" />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:id="@+id/footer"
+ android:layout_width="fill_parent"
+ android:layout_height="48dp" >
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_alignParentTop="true"
+ android:background="?android:attr/dividerVertical" />
+
+ <View
+ android:id="@+id/horizontal_divider"
+ android:layout_width="1dip"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="4dp"
+ android:layout_marginTop="4dp"
+ android:background="?android:attr/dividerVertical" />
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/cancel_label" />
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/horizontal_divider"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/confirm_label" />
+ </RelativeLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml
new file mode 100644
index 000000000..acde9d786
--- /dev/null
+++ b/app/src/main/res/layout/about.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <WebView
+ android:id="@+id/webvAbout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml
new file mode 100644
index 000000000..09502eb7b
--- /dev/null
+++ b/app/src/main/res/layout/addfeed.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentTop="true"
+ android:scrollbars="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/txtvFeedurl"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_margin="16dp"
+ style="@style/AntennaPod.TextView.Heading"
+ android:text="@string/txtvfeedurl_label"/>
+
+ <EditText
+ android:id="@+id/etxtFeedurl"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvFeedurl"
+ android:layout_margin="8dp"
+ android:hint="@string/etxtFeedurlHint"
+ android:inputType="textUri"/>
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/etxtFeedurl"
+ android:layout_margin="8dp"
+ android:text="@string/confirm_label"/>
+
+ <TextView
+ android:id="@+id/txtvPodcastDirectories"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/butConfirm"
+ android:layout_margin="8dp"
+ style="@style/AntennaPod.TextView.Heading"
+ android:text="@string/podcastdirectories_label"/>
+
+ <TextView
+ android:id="@+id/txtvPodcastDirectoriesDescr"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/podcastdirectories_descr"
+ android:textSize="@dimen/text_size_medium"
+ android:layout_below="@id/txtvPodcastDirectories"
+ android:layout_margin="8dp"/>
+
+ <Button
+ android:id="@+id/butBrowseGpoddernet"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvPodcastDirectoriesDescr"
+ android:layout_margin="8dp"
+ android:text="@string/browse_gpoddernet_label"/>
+
+
+ <TextView
+ android:id="@+id/txtvOpmlImport"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/butBrowseGpoddernet"
+ android:layout_margin="8dp"
+ style="@style/AntennaPod.TextView.Heading"
+ android:text="@string/opml_import_label"/>
+
+ <TextView
+ android:id="@+id/txtvOpmlImportExpl"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvOpmlImport"
+ android:layout_margin="8dp"
+ android:textSize="@dimen/text_size_medium"
+ android:text="@string/opml_import_txtv_button_lable"/>
+
+ <Button
+ android:id="@+id/butOpmlImport"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvOpmlImportExpl"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:text="@string/opml_import_label"/>
+ </RelativeLayout>
+ </ScrollView>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/audioplayer_activity.xml b/app/src/main/res/layout/audioplayer_activity.xml
new file mode 100644
index 000000000..a879aad55
--- /dev/null
+++ b/app/src/main/res/layout/audioplayer_activity.xml
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.DrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/non_transparent_background"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/navBar"
+ android:layout_width="fill_parent"
+ android:layout_height="60dp"
+ android:layout_alignParentTop="true">
+
+ <ImageButton
+ android:id="@+id/butNavLeft"
+ android:contentDescription="@string/show_shownotes_label"
+ android:layout_width="60dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:background="?attr/borderless_button"
+ android:padding="4dp"/>
+
+ <ImageButton
+ android:id="@+id/butNavRight"
+ android:contentDescription="@string/show_chapters_label"
+ android:layout_width="60dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:background="?attr/borderless_button"
+ android:padding="4dp"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_toLeftOf="@id/butNavRight"
+ android:layout_toRightOf="@id/butNavLeft"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:maxLines="2"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ android:fontFamily="sans-serif-light"
+ />
+ </RelativeLayout>
+
+ <View
+ android:id="@+id/navBarDivider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/navBar"
+ android:background="@color/bright_blue"/>
+
+ <RelativeLayout
+ android:id="@+id/player_control"
+ android:layout_width="match_parent"
+ android:layout_height="80dp"
+ android:layout_alignParentBottom="true"
+ android:background="?attr/overlay_background">
+
+ <ImageButton
+ android:id="@+id/butPlay"
+ android:contentDescription="@string/pause_label"
+ android:layout_width="80dp"
+ android:layout_height="match_parent"
+ android:layout_centerHorizontal="true"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_pause"/>
+
+ <ImageButton
+ android:id="@+id/butRev"
+ android:contentDescription="@string/rewind_label"
+ android:layout_width="80dp"
+ android:layout_height="match_parent"
+ android:layout_toLeftOf="@id/butPlay"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_rewind"/>
+
+ <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"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_fast_forward"/>
+
+ <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"
+ android:background="?attr/borderless_button"
+ android:src="?attr/av_fast_forward"
+ android:textColor="@color/gray"
+ android:textSize="@dimen/text_size_medium"
+ android:visibility="gone"/>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/playtime_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@id/player_control"
+ android:layout_alignParentLeft="true"
+ android:background="?attr/overlay_drawable">
+
+ <TextView
+ android:id="@+id/txtvPosition"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="16dp"
+ android:text="@string/position_default_label"
+ android:textColor="?android:attr/textColorSecondary"
+ android:fontFamily="sans-serif-light"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <TextView
+ android:id="@+id/txtvLength"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="16dp"
+ android:text="@string/position_default_label"
+ android:textColor="?android:attr/textColorSecondary"
+ android:fontFamily="sans-serif-light"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <SeekBar
+ android:id="@+id/sbPosition"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_toLeftOf="@id/txtvLength"
+ android:layout_toRightOf="@id/txtvPosition"
+ android:max="500"/>
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/contentView"
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_above="@id/playtime_layout"
+ android:layout_below="@id/navBarDivider">
+ </FrameLayout>
+
+ </RelativeLayout>
+
+ <ListView
+ android:id="@+id/nav_list"
+ android:layout_width="@dimen/drawer_width"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:choiceMode="singleChoice"
+ android:background="?attr/nav_drawer_background"
+ android:scrollbarStyle="outsideOverlay"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"/>
+
+</android.support.v4.widget.DrawerLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/authentication_dialog.xml b/app/src/main/res/layout/authentication_dialog.xml
new file mode 100644
index 000000000..82260eb43
--- /dev/null
+++ b/app/src/main/res/layout/authentication_dialog.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <EditText
+ android:id="@+id/etxtUsername"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_margin="16dp"
+ android:hint="@string/username_label"/>
+
+ <EditText
+ android:id="@+id/etxtPassword"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_margin="16dp"
+ android:inputType="textPassword"
+ android:hint="@string/password_label"/>
+
+ <CheckBox
+ android:id="@+id/chkSaveUsernamePassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/save_username_password_label"/>
+
+
+ </LinearLayout>
+
+ <LinearLayout
+ style="@android:style/ButtonBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8dp"
+ android:text="@string/confirm_label"
+ android:layout_weight="1"/>
+
+ <Button
+ android:id="@+id/butCancel"
+ android:text="@string/cancel_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/autoflattr_preference_dialog.xml b/app/src/main/res/layout/autoflattr_preference_dialog.xml
new file mode 100644
index 000000000..fc2df30d7
--- /dev/null
+++ b/app/src/main/res/layout/autoflattr_preference_dialog.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/chkAutoFlattr"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:text="@string/auto_flattr_enable"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small" />
+
+ <SeekBar
+ android:id="@+id/skbPercent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:max="100" />
+
+ <TextView
+ android:id="@+id/txtvStatus"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:ellipsize="end"
+ android:lines="2"
+ android:text="@string/auto_flattr_after_percent"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/cover_fragment.xml b/app/src/main/res/layout/cover_fragment.xml
new file mode 100644
index 000000000..f9c88ac02
--- /dev/null
+++ b/app/src/main/res/layout/cover_fragment.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/cover_fragment_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <ImageView
+ android:id="@+id/imgvCover"
+ android:contentDescription="@string/cover_label"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:adjustViewBounds="true"
+ android:scaleType="centerInside" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/directory_chooser.xml b/app/src/main/res/layout/directory_chooser.xml
new file mode 100644
index 000000000..738c00842
--- /dev/null
+++ b/app/src/main/res/layout/directory_chooser.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:id="@+id/footer"
+ style="@android:style/ButtonBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:orientation="horizontal" >
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/confirm_label" />
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/cancel_label" />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:id="@+id/directory_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true" >
+
+ <ImageButton
+ android:id="@+id/butNavUp"
+ android:contentDescription="@string/navigate_upwards_label"
+ android:layout_width="60dp"
+ android:layout_height="60dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="?attr/borderless_button"
+ android:src="?attr/navigation_up" />
+
+ <TextView
+ android:id="@+id/txtvSelectedFolderLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_toRightOf="@id/butNavUp"
+ android:text="@string/selected_folder_label"
+ android:textStyle="bold" >
+ </TextView>
+
+ <TextView
+ android:id="@+id/txtvSelectedFolder"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_below="@id/txtvSelectedFolderLabel"
+ android:layout_margin="8dp"
+ android:layout_toRightOf="@id/butNavUp"
+ android:ellipsize="start"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/butNavUp"
+ android:background="@color/bright_blue" />
+ </RelativeLayout>
+
+ <ListView
+ android:id="@+id/directory_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_above="@id/footer"
+ android:layout_below="@id/directory_info" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/download_authentication_activity.xml b/app/src/main/res/layout/download_authentication_activity.xml
new file mode 100644
index 000000000..69106c9b3
--- /dev/null
+++ b/app/src/main/res/layout/download_authentication_activity.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/authentication_notification_title"
+ android:layout_alignParentTop="true"
+ android:textSize="@dimen/text_size_large"
+ android:layout_margin="16dp"
+ android:textColor="@color/bright_blue"
+ android:textStyle="italic"/>
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/authentication_notification_msg"
+ android:layout_below="@id/txtvTitle"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="16dp"/>
+
+ <EditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:id="@+id/etxtUsername"
+ android:hint="@string/username_label"
+ android:layout_below="@id/txtvDescription"/>
+
+ <EditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:id="@+id/etxtPassword"
+ android:hint="@string/password_label"
+ android:inputType="textPassword"
+ android:layout_below="@id/etxtUsername"/>
+
+ <LinearLayout
+ android:id="@+id/footer"
+ style="@android:style/ButtonBar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/confirm_label"/>
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/cancel_label"/>
+ </LinearLayout>
+
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/downloaded_episodeslist_item.xml b/app/src/main/res/layout/downloaded_episodeslist_item.xml
new file mode 100644
index 000000000..97003ce65
--- /dev/null
+++ b/app/src/main/res/layout/downloaded_episodeslist_item.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_marginRight="8dp">
+
+ <ImageView
+ android:id="@+id/imgvImage"
+ android:contentDescription="@string/cover_label"
+ android:layout_width="@dimen/thumbnail_length_downloaded_item"
+ android:layout_height="@dimen/thumbnail_length_downloaded_item"
+ android:layout_alignParentLeft="true"
+ android:scaleType="centerCrop"/>
+
+ <TextView
+ android:id="@+id/txtvPublished"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/imgvImage"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvPublished"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginTop="2dp"
+ android:layout_toRightOf="@id/imgvImage"
+ android:layout_centerVertical="true"
+ android:layout_alignParentRight="true"
+ android:ellipsize="end"
+ android:lines="2"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small"/>
+
+ <TextView
+ android:id="@+id/txtvSize"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+ </RelativeLayout>
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@drawable/vertical_divider"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"/>
+
+ <ImageButton
+ android:id="@+id/butSecondaryAction"
+ android:contentDescription="@string/remove_episode_lable"
+ android:focusable="false"
+ android:clickable="false"
+ android:focusableInTouchMode="false"
+ android:layout_width="@dimen/listview_secondary_button_width"
+ android:layout_height="match_parent"
+ android:background="?attr/borderless_button"
+ android:src="?attr/content_discard"
+ />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/downloadlist_item.xml b/app/src/main/res/layout/downloadlist_item.xml
new file mode 100644
index 000000000..49e0ea471
--- /dev/null
+++ b/app/src/main/res/layout/downloadlist_item.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginTop="8dp"
+ android:textSize="@dimen/text_size_small"
+ android:lines="1"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/txtvMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_micro"
+ android:lines="1"
+ android:ellipsize="end"/>
+
+ <ProgressBar
+ android:id="@+id/progProgress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="match_parent"
+ android:layout_height="16dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp">
+
+ <TextView
+ android:id="@+id/txtvDownloaded"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_size_small"
+ android:lines="1"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorPrimary"
+ android:layout_alignParentLeft="true"/>
+
+ <TextView
+ android:id="@+id/txtvPercent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_size_small"
+ android:lines="1"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorPrimary"
+ android:layout_alignParentRight="true"/>
+ </RelativeLayout>
+
+ </LinearLayout>
+
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@drawable/vertical_divider"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"/>
+
+ <ImageButton
+ android:id="@+id/butSecondaryAction"
+ android:contentDescription="@string/cancel_download_label"
+ android:focusable="false"
+ android:clickable="false"
+ android:focusableInTouchMode="false"
+ android:layout_width="@dimen/listview_secondary_button_width"
+ android:layout_height="match_parent"
+ android:background="?attr/borderless_button"
+ android:src="?attr/navigation_cancel"
+ />
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/downloadlog_item.xml b/app/src/main/res/layout/downloadlog_item.xml
new file mode 100644
index 000000000..22d669097
--- /dev/null
+++ b/app/src/main/res/layout/downloadlog_item.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="4dp" >
+
+ <TextView
+ android:id="@+id/txtvType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_marginRight="8dp"
+ android:textSize="@dimen/text_size_small"
+ android:textColor="?android:attr/textColorTertiary" />
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/txtvType"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_medium"
+ android:ellipsize="end"
+ android:maxLines="2" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ >
+ <TextView
+ android:id="@+id/txtvDate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8dp"
+ android:lines="1"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_small"
+ android:layout_alignParentLeft="true"/>
+
+ <TextView
+ android:id="@+id/txtvStatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:textSize="@dimen/text_size_small"
+ android:layout_alignParentRight="true"/>
+
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/txtvReason"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="8dp" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/ellipsize_start_listitem.xml b/app/src/main/res/layout/ellipsize_start_listitem.xml
new file mode 100644
index 000000000..161e1aa37
--- /dev/null
+++ b/app/src/main/res/layout/ellipsize_start_listitem.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:textColor="?android:attr/textColorPrimary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_size_small"
+ android:lines="1"
+ android:singleLine="true"
+ android:layout_margin="16dp"
+ android:ellipsize="start"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/external_itemlist_item.xml b/app/src/main/res/layout/external_itemlist_item.xml
new file mode 100644
index 000000000..20c63c2cf
--- /dev/null
+++ b/app/src/main/res/layout/external_itemlist_item.xml
@@ -0,0 +1,115 @@
+<?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="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"
+ android:scaleType="centerCrop" />
+
+ <ImageButton
+ android:id="@+id/butAction"
+ android:contentDescription="@string/butAction_label"
+ android:layout_width="48dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:background="?attr/borderless_button"
+ android:clickable="false"
+ android:focusable="false"
+ android:focusableInTouchMode="false"
+ android:paddingLeft="24dp"
+ android:paddingRight="8dp"
+ android:paddingTop="16dp"
+ android:scaleType="fitEnd"
+ android:src="?attr/spinner_button" />
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginTop="2dp"
+ android:layout_toLeftOf="@id/butAction"
+ android:layout_toRightOf="@id/imgvFeedimage"
+ android:ellipsize="end"
+ android:lines="2"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_toLeftOf="@id/butAction"
+ android:layout_toRightOf="@id/imgvFeedimage"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/txtvFeedname"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_micro" />
+
+ <RelativeLayout
+ android:id="@+id/bottom_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:id="@+id/txtvLenSize"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro" />
+
+ <ImageView
+ android:id="@+id/imgvDownloadStatus"
+ android:layout_width="@dimen/enc_icons_size"
+ android:layout_height="@dimen/enc_icons_size"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ tools:ignore="ContentDescription"/>
+
+ <ProgressBar
+ android:id="@+id/pbar_episode_progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@id/imgvDownloadStatus"
+ android:layout_toRightOf="@id/txtvLenSize" />
+ </RelativeLayout>
+ </LinearLayout>
+
+ <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"
+ android:layout_alignParentTop="true"
+ android:layout_margin="8dp"
+ android:background="@color/status_playing"
+ android:gravity="center"
+ android:padding="2dp"
+ android:src="@drawable/av_play_dark" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/external_player_fragment.xml b/app/src/main/res/layout/external_player_fragment.xml
new file mode 100644
index 000000000..f084ccac1
--- /dev/null
+++ b/app/src/main/res/layout/external_player_fragment.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragmentLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:background="@color/bright_blue"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <RelativeLayout
+ android:id="@+id/layoutInfo"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:background="?attr/borderless_button">
+
+ <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"
+ android:padding="4dp"
+ android:adjustViewBounds="true"
+ android:cropToPadding="true"
+ android:scaleType="fitXY"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_toRightOf="@id/imgvCover"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textSize="18sp"
+ android:fontFamily="sans-serif-light"/>
+ </RelativeLayout>
+
+ <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"/>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml
new file mode 100644
index 000000000..6da200951
--- /dev/null
+++ b/app/src/main/res/layout/feedinfo.xml
@@ -0,0 +1,204 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/header"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/imgvCover"
+ android:contentDescription="@string/cover_label"
+ android:layout_width="70dp"
+ android:layout_height="70dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_margin="4dp"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_margin="4dp"
+ android:layout_toRightOf="@id/imgvCover"
+ style="@style/AntennaPod.TextView.Heading"/>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/imgvCover"
+ android:background="@color/bright_blue"/>
+ </RelativeLayout>
+
+ <ScrollView
+ android:id="@+id/scrollView"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp">
+
+ <View
+ android:id="@+id/center_divider"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_centerHorizontal="true"/>
+
+ <TextView
+ android:id="@+id/lblAuthor"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_margin="8dp"
+ android:text="@string/author_label"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/txtvAuthor"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_margin="8dp"
+ android:layout_toRightOf="@id/center_divider"/>
+
+ <TextView
+ android:id="@+id/lblLanguage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/txtvAuthor"
+ android:layout_margin="8dp"
+ android:text="@string/language_label"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/txtvLanguage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_below="@id/txtvAuthor"
+ android:layout_margin="8dp"
+ android:layout_toRightOf="@id/center_divider"/>
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/txtvSettings"
+ style="@style/AntennaPod.TextView.Heading"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/podcast_settings_label"
+ android:layout_marginLeft="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="24dp"/>
+
+ <CheckBox
+ android:id="@+id/cbxAutoDownload"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/auto_download_label"
+ android:enabled="false"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/txtvAuthentication"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/authentication_label"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorPrimary"
+ android:layout_marginLeft="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="24dp"/>
+
+ <TextView
+ android:id="@+id/txtvAuthenticationDescr"
+ android:text="@string/authentication_descr"
+ android:textSize="@dimen/text_size_small"
+ android:textColor="?android:attr/textColorPrimary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp">
+
+ <TextView
+ android:id="@+id/txtvUsername"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/username_label"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <EditText
+ android:id="@+id/etxtUsername"
+ android:layout_width="0dp"
+ android:layout_weight="2"
+ android:layout_height="wrap_content"
+ android:hint="@string/username_label"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp">
+
+ <TextView
+ android:id="@+id/txtvPassword"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/password_label"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+ <EditText
+ android:id="@+id/etxtPassword"
+ android:layout_width="0dp"
+ android:layout_weight="2"
+ android:layout_height="wrap_content"
+ android:hint="@string/password_label"
+ android:inputType="textPassword"/>
+ </LinearLayout>
+
+ <TextView
+ style="@style/AntennaPod.TextView.Heading"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="24dp"
+ 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>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/feeditem_dialog.xml b/app/src/main/res/layout/feeditem_dialog.xml
new file mode 100644
index 000000000..e4a37d685
--- /dev/null
+++ b/app/src/main/res/layout/feeditem_dialog.xml
@@ -0,0 +1,72 @@
+<?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="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:id="@+id/txtvTitle"
+ android:layout_alignParentTop="true"
+ style="@style/AntennaPod.Dialog.Title"
+ android:maxLines="5"
+ android:ellipsize="none"/>
+
+ <View
+ android:id="@+id/title_divider"
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:layout_below="@id/txtvTitle"
+ android:background="@color/bright_blue"/>
+
+ <LinearLayout
+ android:id="@+id/header"
+ android:orientation="horizontal"
+ android:layout_below="@id/title_divider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageButton
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:layout_weight="1"
+ android:id="@+id/butAction1"
+ android:background="?attr/borderless_button"
+ tools:ignore="ContentDescription"/>
+
+ <ImageButton
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:layout_weight="1"
+ android:id="@+id/butAction2"
+ android:background="?attr/borderless_button"
+ tools:ignore="ContentDescription"/>
+
+ <ImageButton
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:layout_weight="1"
+ android:id="@+id/butMoreActions"
+ android:background="?attr/borderless_button"
+ android:src="?attr/ic_action_overflow"
+ android:contentDescription="@string/butAction_label"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:layout_below="@id/header"
+ android:background="@color/bright_blue"/>
+
+ <WebView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_below="@id/divider"
+ android:layout_alignParentBottom="true"
+ android:id="@+id/webview"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/feeditemlist_header.xml b/app/src/main/res/layout/feeditemlist_header.xml
new file mode 100644
index 000000000..83c189bb5
--- /dev/null
+++ b/app/src/main/res/layout/feeditemlist_header.xml
@@ -0,0 +1,65 @@
+<?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="match_parent"
+ android:layout_height="match_parent"
+ tools:context="de.danoeh.antennapod.activity.MainActivity">
+
+ <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"
+ android:layout_alignParentTop="true"
+ android:layout_margin="4dp"/>
+
+ <ImageButton
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:id="@+id/butShowInfo"
+ android:contentDescription="@string/show_info_label"
+ android:src="?attr/action_about"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentBottom="true"
+ android:background="?attr/borderless_button"/>
+
+ <ImageButton
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginRight="8dp"
+ android:id="@+id/butVisitWebsite"
+ android:contentDescription="@string/visit_website_label"
+ android:src="?attr/location_web_site"
+ android:layout_toLeftOf="@id/butShowInfo"
+ android:layout_alignParentBottom="true"
+ android:background="?attr/borderless_button"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:layout_alignTop="@id/imgvCover"
+ android:layout_toRightOf="@id/imgvCover"
+ android:layout_alignParentRight="true"
+ android:lines="1"
+ style="@style/AntennaPod.TextView.Heading"
+ android:layout_margin="4dp"/>
+
+ <TextView
+ android:id="@+id/txtvAuthor"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:layout_below="@id/txtvTitle"
+ android:layout_toRightOf="@id/imgvCover"
+ android:layout_toLeftOf="@id/butShowInfo"
+ android:lines="1"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_small"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml
new file mode 100644
index 000000000..f3701de2c
--- /dev/null
+++ b/app/src/main/res/layout/feeditemlist_item.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ android:layout_margin="8dp"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:paddingLeft="4dp">
+
+ <TextView
+ android:id="@+id/txtvPublished"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <TextView
+ android:id="@+id/txtvItemname"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvPublished"
+ android:layout_marginBottom="8dp"
+ style="@style/AntennaPod.TextView.ListItemPrimaryTitle"/>
+
+
+ <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_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:padding="2dp"
+ android:src="?attr/stat_playlist"
+ android:visibility="visible"/>
+
+ <ImageView
+ android:id="@+id/imgvType"
+ android:layout_width="@dimen/enc_icons_size"
+ android:layout_height="@dimen/enc_icons_size"
+ android:layout_alignParentBottom="true"
+ android:layout_toLeftOf="@+id/imgvInPlaylist"
+ android:padding="2dp"
+ tools:ignore="ContentDescription"/>
+
+ <TextView
+ android:id="@+id/txtvLenSize"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/txtvItemname"
+ android:maxLines="2"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <ProgressBar
+ android:id="@+id/pbar_episode_progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvItemname"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginTop="2dp"
+ android:layout_toLeftOf="@id/imgvType"
+ android:layout_toRightOf="@id/txtvLenSize"/>
+
+ <TextView
+ android:id="@+id/statusUnread"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/AntennaPod.TextView.UnreadIndicator"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"/>
+ </RelativeLayout>
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@drawable/vertical_divider"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"/>
+
+ <ImageButton
+ android:id="@+id/butSecondaryAction"
+ android:focusable="false"
+ android:clickable="false"
+ android:focusableInTouchMode="false"
+ android:layout_width="@dimen/listview_secondary_button_width"
+ android:layout_height="match_parent"
+ android:background="?attr/borderless_button"
+ tools:ignore="ContentDescription"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/flattr_auth.xml b/app/src/main/res/layout/flattr_auth.xml
new file mode 100644
index 000000000..9244b786d
--- /dev/null
+++ b/app/src/main/res/layout/flattr_auth.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/txtvExplanation"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/flattr_auth_explanation" />
+
+ <Button
+ android:id="@+id/but_authenticate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="8dp"
+ android:text="@string/authenticate_label" />
+
+ <Button
+ android:id="@+id/but_return_home"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:text="@string/return_home_label"
+ android:visibility="gone" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/gpodnet_podcast_list.xml b/app/src/main/res/layout/gpodnet_podcast_list.xml
new file mode 100644
index 000000000..0112754ee
--- /dev/null
+++ b/app/src/main/res/layout/gpodnet_podcast_list.xml
@@ -0,0 +1,45 @@
+<?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="match_parent"
+ android:layout_height="match_parent">
+
+ <GridView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/gridView"
+ android:stretchMode="columnWidth"
+ android:numColumns="auto_fit"
+ android:verticalSpacing="8dp"
+ android:horizontalSpacing="8dp"
+ android:gravity="center"
+ android:columnWidth="200dp"
+ tools:listitem="@layout/gpodnet_podcast_listitem"/>
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/progressBar"
+ android:layout_centerInParent="true"
+ android:indeterminateOnly="true"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/txtvError"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ android:textAlignment="center"
+ android:layout_margin="16dp"
+ android:textSize="@dimen/text_size_small"/>
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/butRetry"
+ android:text="@string/retry_label"
+ android:layout_margin="16dp"
+ android:visibility="gone"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/txtvError"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/gpodnet_podcast_listitem.xml b/app/src/main/res/layout/gpodnet_podcast_listitem.xml
new file mode 100644
index 000000000..1f6cdd1d0
--- /dev/null
+++ b/app/src/main/res/layout/gpodnet_podcast_listitem.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ 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"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="4dip"
+ android:adjustViewBounds="true"
+ android:cropToPadding="true"
+ android:scaleType="fitXY" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/thumbnail_length_itemlist"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@id/imgvCover"
+ android:layout_marginRight="8dp"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small" />
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro" />
+
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/gpodnetauth_activity.xml b/app/src/main/res/layout/gpodnetauth_activity.xml
new file mode 100644
index 000000000..c096c20cf
--- /dev/null
+++ b/app/src/main/res/layout/gpodnetauth_activity.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+<ViewFlipper
+ android:id="@+id/viewflipper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+</ScrollView> \ No newline at end of file
diff --git a/app/src/main/res/layout/gpodnetauth_credentials.xml b/app/src/main/res/layout/gpodnetauth_credentials.xml
new file mode 100644
index 000000000..3e3c4e54f
--- /dev/null
+++ b/app/src/main/res/layout/gpodnetauth_credentials.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_login_title"
+ android:layout_alignParentTop="true"
+ android:layout_margin="16dp"
+ style="@style/AntennaPod.TextView.Heading"/>
+
+ <TextView
+ android:id="@id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_login_descr"
+ android:layout_below="@id/txtvTitle"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="16dp"/>
+
+ <EditText
+ android:id="@+id/etxtUsername"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/username_label"
+ android:layout_below="@id/txtvDescription"
+ android:layout_margin="8dp"/>
+
+ <EditText
+ android:id="@+id/etxtPassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password_label"
+ android:layout_below="@id/etxtUsername"
+ android:inputType="textPassword"
+ android:layout_margin="8dp"/>
+
+ <Button
+ android:id="@+id/butLogin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/etxtPassword"
+ android:layout_alignParentRight="true"
+ android:text="@string/gpodnetauth_login_butLabel"
+ android:layout_margin="8dp"/>
+
+ <TextView
+ android:id="@+id/txtvError"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/etxtPassword"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@id/butLogin"
+ android:textColor="@color/download_failed_red"
+ android:textSize="@dimen/text_size_small"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:gravity="center"
+ android:layout_margin="16dp"/>
+
+ <ProgressBar
+ android:id="@+id/progBarLogin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:layout_alignTop="@+id/butLogin"
+ android:layout_toLeftOf="@+id/butLogin"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="16dp"
+ android:text="@string/gpodnetauth_login_register"
+ android:autoLink="web"
+ android:layout_below="@id/butLogin"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/gpodnetauth_device.xml b/app/src/main/res/layout/gpodnetauth_device.xml
new file mode 100644
index 000000000..33d3d2718
--- /dev/null
+++ b/app/src/main/res/layout/gpodnetauth_device.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_device_title"
+ android:layout_alignParentTop="true"
+ android:layout_margin="16dp"
+ style="@style/AntennaPod.TextView.Heading"/>
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_device_descr"
+ android:layout_below="@id/txtvTitle"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="16dp"/>
+
+ <EditText
+ android:id="@+id/etxtCaption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/gpodnetauth_device_caption"
+ android:layout_below="@id/txtvDescription"
+ android:layout_margin="8dp"/>
+
+ <TextView
+ android:id="@+id/txtvDeviceID"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_device_deviceID"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="8dp"
+ android:layout_alignTop="@+id/etxtDeviceID"
+ android:layout_alignLeft="@+id/etxtCaption"/>
+
+ <EditText
+ android:id="@+id/etxtDeviceID"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/etxtCaption"
+ android:layout_toRightOf="@id/txtvDeviceID"
+ android:layout_alignParentRight="true"
+ android:layout_margin="8dp"/>
+
+ <Button
+ android:id="@+id/butCreateNewDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:layout_alignParentRight="true"
+ android:layout_below="@id/etxtDeviceID"
+ android:text="@string/gpodnetauth_device_butCreateNewDevice"/>
+
+ <TextView
+ android:id="@+id/txtvError"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/etxtDeviceID"
+ android:layout_toLeftOf="@id/butCreateNewDevice"
+ android:textColor="@color/download_failed_red"
+ android:layout_margin="16dp"
+ android:textSize="@dimen/text_size_small"
+ />
+
+ <ProgressBar
+ android:id="@+id/progbarCreateDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@id/butCreateNewDevice"
+ android:layout_toLeftOf="@id/butCreateNewDevice"
+ android:textColor="@color/download_failed_red"
+ android:textSize="@dimen/text_size_medium"
+ android:visibility="gone"
+ />
+
+ <TextView
+ android:id="@+id/txtvChooseExistingDevice"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_device_chooseExistingDevice"
+ android:layout_below="@id/butCreateNewDevice"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="16dp"/>
+
+ <Button
+ android:id="@+id/butChooseExistingDevice"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_device_butChoose"
+ android:layout_below="@+id/spinnerChooseDevice"
+ android:layout_alignLeft="@+id/butCreateNewDevice"
+ android:layout_alignRight="@+id/butCreateNewDevice"/>
+
+ <Spinner
+ android:id="@+id/spinnerChooseDevice"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvChooseExistingDevice"
+ android:layout_alignParentLeft="true"
+ android:layout_margin="8dp"
+ android:layout_alignParentRight="true"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/gpodnetauth_finish.xml b/app/src/main/res/layout/gpodnetauth_finish.xml
new file mode 100644
index 000000000..71873201a
--- /dev/null
+++ b/app/src/main/res/layout/gpodnetauth_finish.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_finish_title"
+ android:layout_alignParentTop="true"
+ android:layout_margin="16dp"
+ style="@style/AntennaPod.TextView.Heading"/>
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_finish_descr"
+ android:layout_below="@id/txtvTitle"
+ android:textSize="@dimen/text_size_medium"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_margin="16dp"/>
+
+ <Button
+ android:id="@+id/butSyncNow"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvDescription"
+ android:layout_margin="16dp"
+ android:text="@string/gpodnetauth_finish_butsyncnow"/>
+
+ <Button
+ android:id="@+id/butGoMainscreen"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/butSyncNow"
+ android:layout_margin="16dp"
+ android:text="@string/gpodnetauth_finish_butgomainscreen"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/itemdescription_listitem.xml b/app/src/main/res/layout/itemdescription_listitem.xml
new file mode 100644
index 000000000..d6a3f6a16
--- /dev/null
+++ b/app/src/main/res/layout/itemdescription_listitem.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_margin="8dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small"/>
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_margin="8dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lines="3"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/listview_activity.xml b/app/src/main/res/layout/listview_activity.xml
new file mode 100644
index 000000000..b276f506c
--- /dev/null
+++ b/app/src/main/res/layout/listview_activity.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ListView
+ android:id="@+id/listview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml
new file mode 100644
index 000000000..cfd59b87c
--- /dev/null
+++ b/app/src/main/res/layout/main.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.DrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+
+ <FrameLayout
+ android:id="@+id/playerFragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"/>
+
+ <FrameLayout
+ android:id="@+id/main_view"
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_alignParentTop="true"
+ android:layout_above="@id/playerFragment"/>
+
+ </RelativeLayout>
+
+ <ListView
+ android:id="@+id/nav_list"
+ android:layout_width="@dimen/drawer_width"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:choiceMode="singleChoice"
+ android:background="?attr/nav_drawer_background"
+ android:scrollbarStyle="outsideOverlay"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"/>
+
+</android.support.v4.widget.DrawerLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/nav_feedlistitem.xml b/app/src/main/res/layout/nav_feedlistitem.xml
new file mode 100644
index 000000000..d94c9ada1
--- /dev/null
+++ b/app/src/main/res/layout/nav_feedlistitem.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+
+ <ImageView
+ android:id="@+id/imgvCover"
+ android:contentDescription="@string/cover_label"
+ android:layout_width="@dimen/thumbnail_length_navlist"
+ android:layout_height="@dimen/thumbnail_length_navlist"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:adjustViewBounds="true"
+ android:cropToPadding="true"
+ android:scaleType="fitXY"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="6dp"/>
+
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:lines="1"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:layout_centerVertical="true"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_navdrawer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="12dp"
+ android:layout_marginTop="14dp"
+ android:layout_marginBottom="14dp"
+ android:layout_marginRight="48dp"
+ android:layout_toRightOf="@id/imgvCover"
+ />
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/nav_listitem.xml b/app/src/main/res/layout/nav_listitem.xml
new file mode 100644
index 000000000..9d70e7d7c
--- /dev/null
+++ b/app/src/main/res/layout/nav_listitem.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/imgvCover"
+ android:contentDescription="@string/cover_label"
+ android:layout_width="@dimen/thumbnail_length_navlist"
+ android:layout_height="@dimen/thumbnail_length_navlist"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:adjustViewBounds="true"
+ android:cropToPadding="true"
+ android:scaleType="centerCrop"
+ android:padding="8dp"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="6dp"/>
+
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:lines="1"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:layout_centerVertical="true"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_navdrawer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="12dp"
+ android:layout_marginTop="14dp"
+ android:layout_marginBottom="14dp"
+ android:layout_marginRight="48dp"
+ android:layout_toRightOf="@id/imgvCover"
+ />
+
+ <TextView
+ android:id="@+id/txtvCount"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_navdrawer"
+ android:layout_marginLeft="12dp"
+ android:layout_marginTop="14dp"
+ android:layout_marginBottom="14dp"
+ android:layout_marginRight="16dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/nav_section_item.xml b/app/src/main/res/layout/nav_section_item.xml
new file mode 100644
index 000000000..1f2fc7e3e
--- /dev/null
+++ b/app/src/main/res/layout/nav_section_item.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_small"
+ android:typeface="sans"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:layout_marginTop="16dp"
+ android:paddingBottom="4dp"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:layout_alignParentBottom="true"
+ android:background="@color/gray"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/new_episodes_fragment.xml b/app/src/main/res/layout/new_episodes_fragment.xml
new file mode 100644
index 000000000..63c712f57
--- /dev/null
+++ b/app/src/main/res/layout/new_episodes_fragment.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:dslv="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.mobeta.android.dslv.DragSortListView
+ android:id="@android:id/list"
+ android:scrollbarStyle="outsideOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ dslv:collapsed_height="2dp"
+ dslv:drag_enabled="false"
+ dslv:drag_scroll_start="0.33"
+ dslv:float_alpha="0.6"
+ dslv:max_drag_scroll_speed="0.5"
+ dslv:remove_enabled="true"
+ dslv:remove_mode="flingRemove"
+ dslv:slide_shuffle_speed="0.3"
+ dslv:sort_enabled="false"
+ dslv:track_drag_sort="false"
+ dslv:float_background_color="?attr/dragview_float_background"
+ dslv:use_default_controller="true"/>
+
+ <TextView
+ android:id="@id/android:empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:text="@string/no_items_label"/>
+
+ <ProgressBar
+ android:id="@+id/progLoading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminateOnly="true"
+ android:visibility="gone"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/new_episodes_listitem.xml b/app/src/main/res/layout/new_episodes_listitem.xml
new file mode 100644
index 000000000..dcef1f8fc
--- /dev/null
+++ b/app/src/main/res/layout/new_episodes_listitem.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/imgvImage"
+ android:contentDescription="@string/cover_label"
+ android:layout_width="@dimen/thumbnail_length_itemlist"
+ android:layout_height="@dimen/thumbnail_length_itemlist"
+ android:scaleType="centerCrop"/>
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_margin="4dp">
+
+ <TextView
+ android:id="@+id/txtvPublished"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvPublished"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ style="@style/AntennaPod.TextView.ListItemPrimaryTitle"/>
+
+ <TextView
+ android:id="@+id/statusUnread"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ style="@style/AntennaPod.TextView.UnreadIndicator"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentLeft="true"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/bottom_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <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_alignParentRight="true"
+ android:src="?attr/stat_playlist"/>
+
+ <ProgressBar
+ android:id="@+id/pbar_download_progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:max="100"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@id/imgvInPlaylist"
+ android:layout_marginRight="8dp"
+ android:layout_alignParentLeft="true"/>
+
+ <TextView
+ android:id="@+id/txtvDuration"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@id/imgvInPlaylist"
+ android:layout_alignParentLeft="true"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+ </RelativeLayout>
+ </LinearLayout>
+ </RelativeLayout>
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@drawable/vertical_divider"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"/>
+
+ <ImageButton
+ android:id="@+id/butSecondaryAction"
+ android:focusable="false"
+ android:clickable="false"
+ android:focusableInTouchMode="false"
+ android:layout_width="@dimen/listview_secondary_button_width"
+ android:layout_height="match_parent"
+ android:background="?attr/borderless_button"
+ tools:ignore="ContentDescription"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/onlinefeedview_header.xml b/app/src/main/res/layout/onlinefeedview_header.xml
new file mode 100644
index 000000000..11ae1f644
--- /dev/null
+++ b/app/src/main/res/layout/onlinefeedview_header.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <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"
+ android:layout_alignParentTop="true"
+ android:layout_margin="4dp"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:layout_alignTop="@id/imgvCover"
+ android:layout_toRightOf="@id/imgvCover"
+ android:layout_alignParentRight="true"
+ android:lines="1"
+ style="@style/AntennaPod.TextView.Heading"
+ android:layout_margin="4dp"/>
+
+ <TextView
+ android:id="@+id/txtvAuthor"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:layout_below="@id/txtvTitle"
+ android:layout_toRightOf="@id/imgvCover"
+ android:lines="1"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_small"/>
+
+ <Button
+ android:id="@+id/butSubscribe"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:text="@string/subscribe_label"
+ android:layout_below="@id/txtvAuthor"
+ android:layout_alignParentRight="true"
+ android:focusable="false"
+ />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/butSubscribe"
+ android:orientation="vertical">
+
+
+ <Spinner
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/spinnerAlternateUrls"
+ android:layout_margin="4dp"
+ android:textSize="@dimen/text_size_micro"
+ android:textColor="?android:attr/textColorPrimary"/>
+
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLines="3"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:layout_margin="4dp"/>
+
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/opml_import.xml b/app/src/main/res/layout/opml_import.xml
new file mode 100644
index 000000000..919e30551
--- /dev/null
+++ b/app/src/main/res/layout/opml_import.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/opml_import_explanation" />
+
+ <TextView
+ android:id="@+id/txtvPath"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp" />
+
+ <Button
+ android:id="@+id/butStartImport"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="8dp"
+ android:text="@string/start_import_label" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/opml_selection.xml b/app/src/main/res/layout/opml_selection.xml
new file mode 100644
index 000000000..d08ebd0bd
--- /dev/null
+++ b/app/src/main/res/layout/opml_selection.xml
@@ -0,0 +1,39 @@
+<?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="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:id="@+id/footer"
+ style="@android:style/ButtonBar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:orientation="horizontal" >
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/confirm_label" />
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="0px"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/cancel_label" />
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/feedlist"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_above="@id/footer"
+ android:layout_alignParentTop="true"
+ tools:listitem="@android:layout/simple_list_item_multiple_choice" >
+ </ListView>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/pager_fragment.xml b/app/src/main/res/layout/pager_fragment.xml
new file mode 100644
index 000000000..cb7ae0151
--- /dev/null
+++ b/app/src/main/res/layout/pager_fragment.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/player_widget.xml b/app/src/main/res/layout/player_widget.xml
new file mode 100644
index 000000000..b6946f7a8
--- /dev/null
+++ b/app/src/main/res/layout/player_widget.xml
@@ -0,0 +1,52 @@
+<?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:padding="@dimen/widget_margin" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#262C31" >
+
+ <ImageButton
+ android:id="@+id/butPlay"
+ android:contentDescription="@string/play_label"
+ android:layout_width="56dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:layout_margin="12dp"
+ android:background="@drawable/borderless_button_dark"
+ android:src="@drawable/av_play_dark" />
+
+ <LinearLayout
+ android:id="@+id/layout_left"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@id/butPlay"
+ android:background="@drawable/borderless_button_dark"
+ android:gravity="center_vertical"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:maxLines="1"
+ android:text="@string/no_media_playing_label"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/txtvProgress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:textColor="?android:attr/textColorSecondary" />
+ </LinearLayout>
+ </RelativeLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/queue_fragment.xml b/app/src/main/res/layout/queue_fragment.xml
new file mode 100644
index 000000000..742411761
--- /dev/null
+++ b/app/src/main/res/layout/queue_fragment.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:dslv="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.mobeta.android.dslv.DragSortListView
+ android:id="@android:id/list"
+ android:scrollbarStyle="outsideOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ dslv:collapsed_height="2dp"
+ dslv:drag_enabled="true"
+ dslv:drag_handle_id="@id/drag_handle"
+ dslv:drag_scroll_start="0.33"
+ dslv:float_alpha="0.6"
+ dslv:max_drag_scroll_speed="0.5"
+ dslv:remove_enabled="false"
+ dslv:slide_shuffle_speed="0.3"
+ dslv:sort_enabled="true"
+ dslv:track_drag_sort="true"
+ dslv:float_background_color="?attr/dragview_float_background"
+ dslv:use_default_controller="true"/>
+
+ <TextView
+ android:id="@id/android:empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:text="@string/no_items_label"/>
+
+ <ProgressBar
+ android:id="@+id/progLoading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminateOnly="true"
+ android:visibility="gone"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/queue_listitem.xml b/app/src/main/res/layout/queue_listitem.xml
new file mode 100644
index 000000000..4a55cd466
--- /dev/null
+++ b/app/src/main/res/layout/queue_listitem.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="match_parent"
+ android:id="@+id/drag_handle"
+ android:src="?attr/dragview_background"
+ android:scaleType="center"
+ android:layout_margin="8dp"
+ android:contentDescription="@string/drag_handle_content_description"/>
+
+ <ImageView
+ android:id="@+id/imgvImage"
+ android:contentDescription="@string/cover_label"
+ android:layout_width="@dimen/thumbnail_length_queue_item"
+ android:layout_height="@dimen/thumbnail_length_queue_item"
+ android:scaleType="centerCrop"/>
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_margin="8dp">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentLeft="true"
+ android:ellipsize="end"
+ android:lines="2"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/queue_title_text_size"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentLeft="true"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/bottom_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/txtvPosition"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_micro"/>
+
+ <ProgressBar
+ android:id="@+id/pbar_download_progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:max="100"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="8dp"
+ android:layout_toRightOf="@id/txtvPosition"/>
+
+ </RelativeLayout>
+ </LinearLayout>
+ </RelativeLayout>
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@drawable/vertical_divider"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"/>
+
+ <ImageButton
+ android:id="@+id/butSecondaryAction"
+ android:focusable="false"
+ android:clickable="false"
+ android:focusableInTouchMode="false"
+ android:layout_width="@dimen/listview_secondary_button_width"
+ android:layout_height="match_parent"
+ android:background="?attr/borderless_button"
+ tools:ignore="ContentDescription"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/searchlist_item.xml b/app/src/main/res/layout/searchlist_item.xml
new file mode 100644
index 000000000..b057a966d
--- /dev/null
+++ b/app/src/main/res/layout/searchlist_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ 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"
+ android:layout_centerVertical="true"
+ android:scaleType="centerCrop"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@id/imgvFeedimage"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:lines="2"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small"/>
+
+ <TextView
+ android:id="@+id/txtvSubtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:lines="1"
+ android:ellipsize="end"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="@dimen/text_size_small"/>
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/simplechapter_item.xml b/app/src/main/res/layout/simplechapter_item.xml
new file mode 100644
index 000000000..422458d5d
--- /dev/null
+++ b/app/src/main/res/layout/simplechapter_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="12dp"
+ android:paddingTop="12dp">
+
+ <TextView
+ android:id="@+id/txtvStart"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_margin="8dp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_small"/>
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:padding="8dp"
+ android:layout_toLeftOf="@id/txtvStart"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/text_size_small"/>
+
+ <TextView
+ android:id="@+id/txtvLink"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/txtvTitle"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_toLeftOf="@id/txtvStart"
+ android:focusable="false"
+ android:focusableInTouchMode="false"
+ android:visibility="gone"
+ android:maxLines="2" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/storage_error.xml b/app/src/main/res/layout/storage_error.xml
new file mode 100644
index 000000000..c1ee77262
--- /dev/null
+++ b/app/src/main/res/layout/storage_error.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <ImageView
+ android:id="@+id/imageView1"
+ android:contentDescription="@string/external_storage_error_msg"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:layout_margin="16dp"
+ android:src="@android:drawable/stat_notify_sdcard_usb" />
+
+ <TextView
+ android:id="@+id/textView1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/imageView1"
+ android:layout_centerHorizontal="true"
+ android:layout_margin="8dp"
+ android:text="@string/external_storage_error_msg" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/time_dialog.xml b/app/src/main/res/layout/time_dialog.xml
new file mode 100644
index 000000000..95cc9a5a7
--- /dev/null
+++ b/app/src/main/res/layout/time_dialog.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <EditText
+ android:id="@+id/etxtTime"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="8dp"
+ android:ems="7"
+ android:hint="@string/enter_time_here_label"
+ android:inputType="number"
+ android:maxLength="2" >
+
+
+ </EditText>
+
+ <Spinner
+ android:id="@+id/spTimeUnit"
+ android:layout_width="180dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp" />
+ </LinearLayout>
+
+ <LinearLayout
+ style="@android:style/ButtonBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8dp"
+ android:layout_weight="1" />
+
+ <Button
+ android:id="@+id/butCancel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/menu/directory_chooser.xml b/app/src/main/res/menu/directory_chooser.xml
new file mode 100644
index 000000000..7735ffd2c
--- /dev/null
+++ b/app/src/main/res/menu/directory_chooser.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/new_folder_item"
+ android:title="@string/create_folder_label"
+ custom:showAsAction="ifRoom|withText"/>
+ <item
+ android:id="@+id/set_to_default_folder_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/set_to_default_folder"/>
+
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/feedinfo.xml b/app/src/main/res/menu/feedinfo.xml
new file mode 100644
index 000000000..be50cb87d
--- /dev/null
+++ b/app/src/main/res/menu/feedinfo.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/visit_website_item"
+ android:icon="?attr/location_web_site"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/visit_website_label"
+ android:visible="true">
+ </item>
+ <item
+ android:id="@+id/support_item"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/support_label"
+ android:visible="false">
+ </item>
+ <item
+ android:id="@+id/share_link_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/share_link_label">
+ </item>
+ <item
+ android:id="@+id/share_source_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/share_source_label">
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/feeditem.xml b/app/src/main/res/menu/feeditem.xml
new file mode 100644
index 000000000..5b25e8f2c
--- /dev/null
+++ b/app/src/main/res/menu/feeditem.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/download_item"
+ android:icon="?attr/av_download"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/download_label">
+ </item>
+ <item
+ android:id="@+id/stream_item"
+ android:icon="?attr/action_stream"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/stream_label">
+ </item>
+ <item
+ android:id="@+id/play_item"
+ android:icon="?attr/av_play"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/play_label">
+ </item>
+ <item
+ android:id="@+id/remove_item"
+ android:icon="?attr/content_discard"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/remove_label">
+ </item>
+ <item
+ android:id="@id/skip_episode_item"
+ android:title="@string/skip_episode_label"
+ custom:showAsAction="collapseActionView">
+ </item>
+ <item
+ android:id="@+id/cancel_download_item"
+ android:icon="?attr/navigation_cancel"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/cancel_download_label">
+ </item>
+ <item
+ android:id="@+id/mark_read_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/mark_read_label">
+ </item>
+ <item
+ android:id="@+id/mark_unread_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/mark_unread_label">
+ </item>
+ <item
+ android:id="@+id/add_to_queue_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/add_to_queue_label">
+ </item>
+ <item
+ android:id="@+id/remove_from_queue_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/remove_from_queue_label">
+ </item>
+ <item
+ android:id="@+id/share_link_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/share_link_label">
+ </item>
+ <item
+ android:id="@+id/visit_website_item"
+ android:icon="?attr/location_web_site"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/visit_website_label">
+ </item>
+ <item
+ android:id="@+id/support_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/support_label">
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/feeditem_dialog.xml b/app/src/main/res/menu/feeditem_dialog.xml
new file mode 100644
index 000000000..f33b7502a
--- /dev/null
+++ b/app/src/main/res/menu/feeditem_dialog.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@id/skip_episode_item"
+ android:title="@string/skip_episode_label"
+ custom:showAsAction="collapseActionView">
+ </item>
+
+ <item
+ android:id="@+id/mark_read_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/mark_read_label">
+ </item>
+ <item
+ android:id="@+id/mark_unread_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/mark_unread_label">
+ </item>
+ <item
+ android:id="@+id/add_to_queue_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/add_to_queue_label">
+ </item>
+ <item
+ android:id="@+id/remove_from_queue_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/remove_from_queue_label">
+ </item>
+ <item
+ android:id="@+id/share_link_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/share_link_label">
+ </item>
+ <item
+ android:id="@+id/visit_website_item"
+ android:icon="?attr/location_web_site"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/visit_website_label">
+ </item>
+ <item
+ android:id="@+id/support_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/support_label">
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml
new file mode 100644
index 000000000..f8eb7232e
--- /dev/null
+++ b/app/src/main/res/menu/feedlist.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/refresh_item"
+ android:icon="?attr/navigation_refresh"
+ android:menuCategory="container"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/refresh_label">
+ </item>
+ <item
+ android:id="@+id/mark_all_read_item"
+ android:menuCategory="container"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/mark_all_read_label">
+ </item>
+ <item
+ android:id="@+id/support_item"
+ android:menuCategory="container"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/support_label"
+ android:visible="false">
+ </item>
+ <item
+ android:id="@+id/remove_item"
+ android:menuCategory="container"
+ android:icon="?attr/content_discard"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/remove_feed_label"
+ android:visible="true">
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
new file mode 100644
index 000000000..c5b069b40
--- /dev/null
+++ b/app/src/main/res/menu/main.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/show_preferences"
+ android:title="@string/settings_label"
+ android:menuCategory="system"
+ android:icon="?attr/action_settings"
+ custom:showAsAction="collapseActionView"/>
+
+
+</menu>
diff --git a/app/src/main/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml
new file mode 100644
index 000000000..0eb2ab067
--- /dev/null
+++ b/app/src/main/res/menu/mediaplayer.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/disable_sleeptimer_item"
+ android:icon="?attr/device_access_time"
+ custom:showAsAction="always"
+ android:title="@string/sleep_timer_label">
+ </item>
+ <item
+ android:id="@+id/set_sleeptimer_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/set_sleeptimer_label">
+ </item>
+ <item
+ android:id="@+id/share_link_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/share_link_label">
+ </item>
+ <item
+ android:id="@+id/visit_website_item"
+ android:icon="?attr/location_web_site"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/visit_website_label"
+ android:visible="false">
+ </item>
+ <item
+ android:id="@+id/support_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/support_label"
+ android:visible="false">
+ </item>
+ <item
+ android:id="@id/skip_episode_item"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/skip_episode_label"
+ android:visible="true"/>
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/new_episodes.xml b/app/src/main/res/menu/new_episodes.xml
new file mode 100644
index 000000000..4cf3b5fec
--- /dev/null
+++ b/app/src/main/res/menu/new_episodes.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/refresh_item"
+ android:title="@string/refresh_label"
+ android:menuCategory="container"
+ custom:showAsAction="ifRoom|collapseActionView"
+ android:icon="?attr/navigation_refresh"/>
+
+ <item
+ android:id="@+id/mark_all_read_item"
+ android:title="@string/mark_all_read_label"
+ android:menuCategory="container"
+ custom:showAsAction="collapseActionView"
+ android:icon="?attr/navigation_accept"/>
+
+ <item
+ android:id="@+id/episode_filter_item"
+ android:title="@string/episode_filter_label"
+ android:menuCategory="container"
+ android:checkable="true"
+ custom:showAsAction="collapseActionView"/>
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/queue_context.xml b/app/src/main/res/menu/queue_context.xml
new file mode 100644
index 000000000..327600038
--- /dev/null
+++ b/app/src/main/res/menu/queue_context.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/move_to_top_item"
+ android:menuCategory="container"
+ android:title="@string/move_to_top_label" />
+
+ <item
+ android:id="@+id/remove_from_queue_item"
+ android:menuCategory="container"
+ android:title="@string/remove_from_queue_label" />
+
+ <item
+ android:id="@+id/move_to_bottom_item"
+ android:menuCategory="container"
+ android:title="@string/move_to_bottom_label" />
+
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/xml/player_widget_info.xml b/app/src/main/res/xml/player_widget_info.xml
new file mode 100644
index 000000000..831f6daf0
--- /dev/null
+++ b/app/src/main/res/xml/player_widget_info.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:resizeMode="none" android:initialLayout="@layout/player_widget" android:minHeight="40dp" android:minWidth="250dp">
+
+</appwidget-provider> \ No newline at end of file
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
new file mode 100644
index 000000000..5175acdcb
--- /dev/null
+++ b/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <PreferenceCategory android:title="@string/user_interface_label">
+ <ListPreference
+ android:entryValues="@array/theme_values"
+ android:entries="@array/theme_options"
+ android:title="@string/pref_set_theme_title"
+ android:key="prefTheme"
+ android:summary="@string/pref_set_theme_sum"
+ android:defaultValue="0"/>
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefExpandNotify"
+ android:summary="@string/pref_expandNotify_sum"
+ android:title="@string/pref_expandNotify_title"/>
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefPersistNotify"
+ android:summary="@string/pref_persistNotify_sum"
+ android:title="@string/pref_persistNotify_title"/>
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/playback_pref">
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefPauseOnHeadsetDisconnect"
+ android:summary="@string/pref_pauseOnHeadsetDisconnect_sum"
+ android:title="@string/pref_pauseOnHeadsetDisconnect_title"/>
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefFollowQueue"
+ android:summary="@string/pref_followQueue_sum"
+ android:title="@string/pref_followQueue_title"/>
+ <Preference
+ android:key="prefPlaybackSpeedLauncher"
+ android:summary="@string/pref_playback_speed_sum"
+ android:title="@string/pref_playback_speed_title" />
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefPauseForFocusLoss"
+ android:summary="@string/pref_pausePlaybackForFocusLoss_sum"
+ android:title="@string/pref_pausePlaybackForFocusLoss_title" />
+
+ <ListPreference
+ android:defaultValue="30"
+ android:entries="@array/seek_delta_values"
+ android:entryValues="@array/seek_delta_values"
+ android:key="prefSeekDeltaSecs"
+ android:summary="@string/pref_seek_delta_sum"
+ android:title="@string/pref_seek_delta_title" />
+
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/network_pref">
+ <ListPreference
+ android:defaultValue="0"
+ android:entries="@array/update_intervall_values"
+ android:entryValues="@array/update_intervall_values"
+ android:key="prefAutoUpdateIntervall"
+ android:summary="@string/pref_autoUpdateIntervall_sum"
+ android:title="@string/pref_autoUpdateIntervall_title"/>
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefMobileUpdate"
+ android:summary="@string/pref_mobileUpdate_sum"
+ android:title="@string/pref_mobileUpdate_title"/>
+ <ListPreference
+ android:defaultValue="20"
+ android:entries="@array/episode_cache_size_entries"
+ android:key="prefEpisodeCacheSize"
+ android:title="@string/pref_episode_cache_title"
+ android:entryValues="@array/episode_cache_size_values"/>
+ <PreferenceScreen
+ android:summary="@string/pref_automatic_download_sum"
+ android:key="prefAutoDownloadSettings"
+ android:title="@string/pref_automatic_download_title">
+ <CheckBoxPreference
+ android:key="prefEnableAutoDl"
+ android:title="@string/pref_automatic_download_title"
+ android:defaultValue="false"/>
+ <CheckBoxPreference
+ android:key="prefEnableAutoDownloadWifiFilter"
+ android:title="@string/pref_autodl_wifi_filter_title"
+ android:summary="@string/pref_autodl_wifi_filter_sum"/>
+
+ </PreferenceScreen>
+
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/services_label">
+ <PreferenceScreen
+ android:key="prefFlattrSettings"
+ android:title="@string/flattr_label">
+ <PreferenceScreen
+ android:key="pref_flattr_authenticate"
+ android:summary="@string/pref_flattr_auth_sum"
+ android:title="@string/pref_flattr_auth_title">
+ <intent android:action=".activities.FlattrAuthActivity"/>
+ </PreferenceScreen>
+
+ <Preference
+ android:key="prefAutoFlattrPrefs"
+ android:summary="@string/pref_auto_flattr_sum"
+ android:title="@string/pref_auto_flattr_title" />
+ <Preference
+ android:key="prefRevokeAccess"
+ android:summary="@string/pref_revokeAccess_sum"
+ android:title="@string/pref_revokeAccess_title"/>
+ </PreferenceScreen>
+ <PreferenceScreen
+ android:key="prefFlattrSettings"
+ android:title="@string/gpodnet_main_label">
+
+ <PreferenceScreen
+ android:key="pref_gpodnet_authenticate"
+ android:title="@string/pref_gpodnet_authenticate_title"
+ android:summary="@string/pref_gpodnet_authenticate_sum">
+ <intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/>
+ </PreferenceScreen>
+ <Preference
+ android:key="pref_gpodnet_setlogin_information"
+ android:title="@string/pref_gpodnet_setlogin_information_title"
+ android:summary="@string/pref_gpodnet_setlogin_information_sum"/>
+ <Preference
+ android:key="pref_gpodnet_logout"
+ android:title="@string/pref_gpodnet_logout_title"/>
+ <Preference
+ android:key="pref_gpodnet_hostname"
+ android:title="@string/pref_gpodnet_sethostname_title"/>
+ </PreferenceScreen>
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/other_pref">
+ <Preference
+ android:title="@string/choose_data_directory"
+ android:key="prefChooseDataDir"/>
+ <Preference
+ android:key="prefFlattrThisApp"
+ android:summary="@string/pref_flattr_this_app_sum"
+ android:title="@string/pref_flattr_this_app_title">
+ </Preference>
+ <Preference
+ android:key="prefOpmlExport"
+ android:title="@string/opml_export_label"/>
+ <Preference
+ android:key="prefAbout"
+ android:title="@string/about_pref"/>
+
+
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml
new file mode 100644
index 000000000..ee73aca8d
--- /dev/null
+++ b/app/src/main/res/xml/searchable.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:hint="@string/search_hint"
+ android:label="@string/app_name"/> \ No newline at end of file