summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--.tx/config48
-rw-r--r--AndroidManifest.xml348
-rw-r--r--app/build.gradle100
m---------app/dslv0
-rw-r--r--app/lint.xml (renamed from lint.xml)0
-rw-r--r--app/proguard.cfg (renamed from proguard.cfg)0
-rw-r--r--app/settings.gradle1
-rw-r--r--app/src/androidTest/assets/testfile.mp3 (renamed from assets/testfile.mp3)bin20606 -> 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.html (renamed from assets/LICENSE.html)0
-rw-r--r--app/src/main/assets/LICENSE_APACHE_COMMONS.txt (renamed from assets/LICENSE_APACHE_COMMONS.txt)0
-rw-r--r--app/src/main/assets/LICENSE_BETTERPICKERS.txt (renamed from assets/LICENSE_BETTERPICKERS.txt)0
-rw-r--r--app/src/main/assets/LICENSE_DSLV.txt (renamed from assets/LICENSE_DSLV.txt)0
-rw-r--r--app/src/main/assets/LICENSE_FLATTR4J.txt (renamed from assets/LICENSE_FLATTR4J.txt)0
-rw-r--r--app/src/main/assets/LICENSE_JSOUP.txt (renamed from assets/LICENSE_JSOUP.txt)0
-rw-r--r--app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt (renamed from assets/LICENSE_NINE_OLD_ANDROIDS.txt)0
-rw-r--r--app/src/main/assets/LICENSE_OKHTTP.txt (renamed from assets/LICENSE_OKHTTP.txt)0
-rw-r--r--app/src/main/assets/LICENSE_OKIO.txt (renamed from assets/LICENSE_OKIO.txt)0
-rw-r--r--app/src/main/assets/LICENSE_PICASSO.txt (renamed from assets/LICENSE_PICASSO.txt)0
-rw-r--r--app/src/main/assets/LICENSE_PRESTO.txt (renamed from assets/LICENSE_PRESTO.txt)0
-rw-r--r--app/src/main/assets/Roboto-Light.ttf (renamed from assets/Roboto-Light.ttf)bin115200 -> 115200 bytes
-rw-r--r--app/src/main/assets/Roboto.ttf (renamed from assets/Roboto.ttf)bin114976 -> 114976 bytes
-rw-r--r--app/src/main/assets/about.html (renamed from assets/about.html)0
-rwxr-xr-xapp/src/main/assets/logo.png (renamed from assets/logo.png)bin58799 -> 58799 bytes
-rw-r--r--app/src/main/assets/testfile.mp3bin0 -> 20606 bytes
-rw-r--r--app/src/main/java/de/danoeh/antennapod/AppConfig.java (renamed from src/de/danoeh/antennapod/AppConfig.java)0
-rw-r--r--app/src/main/java/de/danoeh/antennapod/PodcastApp.java55
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java (renamed from src/de/danoeh/antennapod/activity/AboutActivity.java)0
-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.java (renamed from src/de/danoeh/antennapod/dialog/AuthenticationDialog.java)0
-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.java (renamed from src/de/danoeh/antennapod/fragment/AddFeedFragment.java)0
-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.java (renamed from src/de/danoeh/antennapod/fragment/DownloadsFragment.java)0
-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.java (renamed from src/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java)0
-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.java (renamed from src/de/danoeh/antennapod/spa/SPAUtil.java)0
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java (renamed from src/de/danoeh/antennapod/view/AspectRatioVideoView.java)0
-rw-r--r--app/src/main/res/anim/fade_in.xml (renamed from res/anim/fade_in.xml)0
-rw-r--r--app/src/main/res/anim/fade_out.xml (renamed from res/anim/fade_out.xml)0
-rw-r--r--app/src/main/res/layout-land/audioplayer_activity.xml (renamed from res/layout-land/audioplayer_activity.xml)0
-rw-r--r--app/src/main/res/layout-land/videoplayer_activity.xml (renamed from res/layout-land/videoplayer_activity.xml)0
-rw-r--r--app/src/main/res/layout-v14/authentication_dialog.xml (renamed from res/layout-v14/authentication_dialog.xml)0
-rw-r--r--app/src/main/res/layout-v14/directory_chooser.xml (renamed from res/layout-v14/directory_chooser.xml)0
-rw-r--r--app/src/main/res/layout-v14/download_authentication_activity.xml (renamed from res/layout-v14/download_authentication_activity.xml)0
-rw-r--r--app/src/main/res/layout-v14/opml_selection.xml (renamed from res/layout-v14/opml_selection.xml)0
-rw-r--r--app/src/main/res/layout-v14/time_dialog.xml (renamed from res/layout-v14/time_dialog.xml)0
-rw-r--r--app/src/main/res/layout/about.xml (renamed from res/layout/about.xml)0
-rw-r--r--app/src/main/res/layout/addfeed.xml (renamed from res/layout/addfeed.xml)0
-rw-r--r--app/src/main/res/layout/audioplayer_activity.xml (renamed from res/layout/audioplayer_activity.xml)0
-rw-r--r--app/src/main/res/layout/authentication_dialog.xml (renamed from res/layout/authentication_dialog.xml)0
-rw-r--r--app/src/main/res/layout/autoflattr_preference_dialog.xml (renamed from res/layout/autoflattr_preference_dialog.xml)0
-rw-r--r--app/src/main/res/layout/cover_fragment.xml (renamed from res/layout/cover_fragment.xml)0
-rw-r--r--app/src/main/res/layout/directory_chooser.xml (renamed from res/layout/directory_chooser.xml)0
-rw-r--r--app/src/main/res/layout/download_authentication_activity.xml (renamed from res/layout/download_authentication_activity.xml)0
-rw-r--r--app/src/main/res/layout/downloaded_episodeslist_item.xml (renamed from res/layout/downloaded_episodeslist_item.xml)0
-rw-r--r--app/src/main/res/layout/downloadlist_item.xml (renamed from res/layout/downloadlist_item.xml)0
-rw-r--r--app/src/main/res/layout/downloadlog_item.xml (renamed from res/layout/downloadlog_item.xml)0
-rw-r--r--app/src/main/res/layout/ellipsize_start_listitem.xml (renamed from res/layout/ellipsize_start_listitem.xml)0
-rw-r--r--app/src/main/res/layout/external_itemlist_item.xml (renamed from res/layout/external_itemlist_item.xml)0
-rw-r--r--app/src/main/res/layout/external_player_fragment.xml (renamed from res/layout/external_player_fragment.xml)0
-rw-r--r--app/src/main/res/layout/feedinfo.xml (renamed from res/layout/feedinfo.xml)0
-rw-r--r--app/src/main/res/layout/feeditem_dialog.xml (renamed from res/layout/feeditem_dialog.xml)0
-rw-r--r--app/src/main/res/layout/feeditemlist_header.xml (renamed from res/layout/feeditemlist_header.xml)0
-rw-r--r--app/src/main/res/layout/feeditemlist_item.xml (renamed from res/layout/feeditemlist_item.xml)0
-rw-r--r--app/src/main/res/layout/flattr_auth.xml (renamed from res/layout/flattr_auth.xml)0
-rw-r--r--app/src/main/res/layout/gpodnet_podcast_list.xml (renamed from res/layout/gpodnet_podcast_list.xml)0
-rw-r--r--app/src/main/res/layout/gpodnet_podcast_listitem.xml (renamed from res/layout/gpodnet_podcast_listitem.xml)0
-rw-r--r--app/src/main/res/layout/gpodnetauth_activity.xml (renamed from res/layout/gpodnetauth_activity.xml)0
-rw-r--r--app/src/main/res/layout/gpodnetauth_credentials.xml (renamed from res/layout/gpodnetauth_credentials.xml)0
-rw-r--r--app/src/main/res/layout/gpodnetauth_device.xml (renamed from res/layout/gpodnetauth_device.xml)0
-rw-r--r--app/src/main/res/layout/gpodnetauth_finish.xml (renamed from res/layout/gpodnetauth_finish.xml)0
-rw-r--r--app/src/main/res/layout/itemdescription_listitem.xml (renamed from res/layout/itemdescription_listitem.xml)0
-rw-r--r--app/src/main/res/layout/listview_activity.xml (renamed from res/layout/listview_activity.xml)0
-rw-r--r--app/src/main/res/layout/main.xml (renamed from res/layout/main.xml)0
-rw-r--r--app/src/main/res/layout/nav_feedlistitem.xml (renamed from res/layout/nav_feedlistitem.xml)0
-rw-r--r--app/src/main/res/layout/nav_listitem.xml (renamed from res/layout/nav_listitem.xml)0
-rw-r--r--app/src/main/res/layout/nav_section_item.xml (renamed from res/layout/nav_section_item.xml)0
-rw-r--r--app/src/main/res/layout/new_episodes_fragment.xml (renamed from res/layout/new_episodes_fragment.xml)0
-rw-r--r--app/src/main/res/layout/new_episodes_listitem.xml (renamed from res/layout/new_episodes_listitem.xml)0
-rw-r--r--app/src/main/res/layout/onlinefeedview_header.xml (renamed from res/layout/onlinefeedview_header.xml)0
-rw-r--r--app/src/main/res/layout/opml_import.xml (renamed from res/layout/opml_import.xml)0
-rw-r--r--app/src/main/res/layout/opml_selection.xml (renamed from res/layout/opml_selection.xml)0
-rw-r--r--app/src/main/res/layout/pager_fragment.xml (renamed from res/layout/pager_fragment.xml)0
-rw-r--r--app/src/main/res/layout/player_widget.xml (renamed from res/layout/player_widget.xml)0
-rw-r--r--app/src/main/res/layout/queue_fragment.xml (renamed from res/layout/queue_fragment.xml)0
-rw-r--r--app/src/main/res/layout/queue_listitem.xml (renamed from res/layout/queue_listitem.xml)0
-rw-r--r--app/src/main/res/layout/searchlist_item.xml (renamed from res/layout/searchlist_item.xml)0
-rw-r--r--app/src/main/res/layout/simplechapter_item.xml (renamed from res/layout/simplechapter_item.xml)0
-rw-r--r--app/src/main/res/layout/storage_error.xml (renamed from res/layout/storage_error.xml)0
-rw-r--r--app/src/main/res/layout/time_dialog.xml (renamed from res/layout/time_dialog.xml)0
-rw-r--r--app/src/main/res/menu/directory_chooser.xml (renamed from res/menu/directory_chooser.xml)0
-rw-r--r--app/src/main/res/menu/feedinfo.xml (renamed from res/menu/feedinfo.xml)0
-rw-r--r--app/src/main/res/menu/feeditem.xml (renamed from res/menu/feeditem.xml)0
-rw-r--r--app/src/main/res/menu/feeditem_dialog.xml (renamed from res/menu/feeditem_dialog.xml)0
-rw-r--r--app/src/main/res/menu/feedlist.xml (renamed from res/menu/feedlist.xml)0
-rw-r--r--app/src/main/res/menu/main.xml (renamed from res/menu/main.xml)0
-rw-r--r--app/src/main/res/menu/mediaplayer.xml (renamed from res/menu/mediaplayer.xml)0
-rw-r--r--app/src/main/res/menu/new_episodes.xml (renamed from res/menu/new_episodes.xml)0
-rw-r--r--app/src/main/res/menu/queue_context.xml (renamed from res/menu/queue_context.xml)0
-rw-r--r--app/src/main/res/xml/player_widget_info.xml (renamed from res/xml/player_widget_info.xml)0
-rw-r--r--app/src/main/res/xml/preferences.xml (renamed from res/xml/preferences.xml)0
-rw-r--r--app/src/main/res/xml/searchable.xml (renamed from res/xml/searchable.xml)0
-rw-r--r--build.gradle127
-rw-r--r--core/.gitignore1
-rw-r--r--core/build.gradle43
-rw-r--r--core/proguard-rules.pro17
-rw-r--r--core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java13
-rw-r--r--core/src/main/AndroidManifest.xml52
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IDeathCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl (renamed from src/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl)0
-rw-r--r--core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl (renamed from src/com/aocate/presto/service/IPlayMedia_0_8.aidl)0
-rw-r--r--core/src/main/java/com/aocate/media/AndroidMediaPlayer.java (renamed from src/com/aocate/media/AndroidMediaPlayer.java)0
-rw-r--r--core/src/main/java/com/aocate/media/MediaPlayer.java1278
-rw-r--r--core/src/main/java/com/aocate/media/MediaPlayerImpl.java (renamed from src/com/aocate/media/MediaPlayerImpl.java)0
-rw-r--r--core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java1201
-rw-r--r--core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java (renamed from src/com/aocate/media/SpeedAdjustmentAlgorithm.java)0
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java58
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java36
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java27
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java27
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java177
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java74
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java237
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java47
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java92
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java37
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java152
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java211
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java64
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java55
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java140
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java445
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java66
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java105
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java72
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java334
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java412
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java89
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java36
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java109
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java718
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java12
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java19
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java72
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java65
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java41
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java56
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java87
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java65
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java247
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java146
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java607
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java32
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java251
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java54
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java98
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java209
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java1208
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java181
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java73
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java252
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java1141
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java979
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java384
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java908
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java898
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java979
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java366
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java70
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java57
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java1310
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java19
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java111
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java126
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java111
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java75
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java68
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java149
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java52
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java202
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java161
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java42
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java274
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Converter.java103
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java52
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java117
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java49
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java36
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java120
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java69
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java93
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java16
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java67
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java54
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java137
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java19
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java19
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java14
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java68
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java304
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java55
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java118
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java250
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java29
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java26
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java235
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java69
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java207
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java790
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java161
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java67
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java78
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java81
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java101
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java26
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java194
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java24
-rw-r--r--core/src/main/res/drawable-hdpi-v11/ic_stat_antenna_default.png (renamed from res/drawable-hdpi-v11/ic_stat_antenna.png)bin678 -> 678 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png (renamed from res/drawable-hdpi-v11/ic_stat_authentication.png)bin467 -> 467 bytes
-rw-r--r--core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png (renamed from res/drawable-hdpi-v11/stat_notify_sync.png)bin1012 -> 1012 bytes
-rw-r--r--core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png (renamed from res/drawable-hdpi-v11/stat_notify_sync_error.png)bin1103 -> 1103 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/action_about.png (renamed from res/drawable-hdpi/action_about.png)bin1764 -> 1764 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/action_about_dark.png (renamed from res/drawable-hdpi/action_about_dark.png)bin1629 -> 1629 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/action_search.png (renamed from res/drawable-hdpi/action_search.png)bin1759 -> 1759 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/action_search_dark.png (renamed from res/drawable-hdpi/action_search_dark.png)bin1764 -> 1764 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/action_settings.png (renamed from res/drawable-hdpi/action_settings.png)bin1505 -> 1505 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/action_settings_dark.png (renamed from res/drawable-hdpi/action_settings_dark.png)bin1540 -> 1540 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/action_stream.png (renamed from res/drawable-hdpi/action_stream.png)bin803 -> 803 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/action_stream_dark.png (renamed from res/drawable-hdpi/action_stream_dark.png)bin693 -> 693 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/av_download.png (renamed from res/drawable-hdpi/av_download.png)bin1328 -> 1328 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/av_download_dark.png (renamed from res/drawable-hdpi/av_download_dark.png)bin1331 -> 1331 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/av_fast_forward.png (renamed from res/drawable-hdpi/av_fast_forward.png)bin1416 -> 1416 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/av_fast_forward_dark.png (renamed from res/drawable-hdpi/av_fast_forward_dark.png)bin1366 -> 1366 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/av_pause.png (renamed from res/drawable-hdpi/av_pause.png)bin1116 -> 1116 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/av_pause_dark.png (renamed from res/drawable-hdpi/av_pause_dark.png)bin1114 -> 1114 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/av_play.png (renamed from res/drawable-hdpi/av_play.png)bin1405 -> 1405 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/av_play_dark.png (renamed from res/drawable-hdpi/av_play_dark.png)bin1410 -> 1410 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/av_rewind.png (renamed from res/drawable-hdpi/av_rewind.png)bin1426 -> 1426 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/av_rewind_dark.png (renamed from res/drawable-hdpi/av_rewind_dark.png)bin1449 -> 1449 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/content_discard.png (renamed from res/drawable-hdpi/content_discard.png)bin1624 -> 1624 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/content_discard_dark.png (renamed from res/drawable-hdpi/content_discard_dark.png)bin1611 -> 1611 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/content_new.png (renamed from res/drawable-hdpi/content_new.png)bin1157 -> 1157 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/content_new_dark.png (renamed from res/drawable-hdpi/content_new_dark.png)bin1142 -> 1142 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/default_cover.png (renamed from res/drawable-hdpi/default_cover.png)bin1404 -> 1404 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/default_cover_dark.png (renamed from res/drawable-hdpi/default_cover_dark.png)bin1426 -> 1426 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/device_access_time.png (renamed from res/drawable-hdpi/device_access_time.png)bin1875 -> 1875 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/device_access_time_dark.png (renamed from res/drawable-hdpi/device_access_time_dark.png)bin1794 -> 1794 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_action_overflow.png (renamed from res/drawable-hdpi/ic_action_overflow.png)bin225 -> 225 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_action_overflow_dark.png (renamed from res/drawable-hdpi/ic_action_overflow_dark.png)bin217 -> 217 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/ic_action_pause_over_video.png (renamed from res/drawable-hdpi/ic_action_pause_over_video.png)bin6552 -> 6552 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/ic_action_play_over_video.png (renamed from res/drawable-hdpi/ic_action_play_over_video.png)bin7123 -> 7123 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/ic_drag_handle.png (renamed from res/drawable-hdpi/ic_drag_handle.png)bin220 -> 220 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/ic_drag_handle_dark.png (renamed from res/drawable-hdpi/ic_drag_handle_dark.png)bin204 -> 204 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_drawer.png (renamed from res/drawable-hdpi/ic_drawer.png)bin2829 -> 2829 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_drawer_dark.png (renamed from res/drawable-hdpi/ic_drawer_dark.png)bin2826 -> 2826 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_launcher.png (renamed from res/drawable-hdpi/ic_launcher.png)bin3955 -> 3955 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/ic_new.png (renamed from res/drawable-hdpi/ic_new.png)bin891 -> 891 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/ic_new_dark.png (renamed from res/drawable-hdpi/ic_new_dark.png)bin716 -> 716 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_stat_antenna_default.png (renamed from res/drawable-hdpi/ic_stat_antenna.png)bin649 -> 649 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/ic_stat_authentication.png (renamed from res/drawable-hdpi/ic_stat_authentication.png)bin648 -> 648 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/location_web_site.png (renamed from res/drawable-hdpi/location_web_site.png)bin2529 -> 2529 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/location_web_site_dark.png (renamed from res/drawable-hdpi/location_web_site_dark.png)bin2516 -> 2516 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/navigation_accept.png (renamed from res/drawable-hdpi/navigation_accept.png)bin1320 -> 1320 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_accept_dark.png (renamed from res/drawable-hdpi/navigation_accept_dark.png)bin1335 -> 1335 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/navigation_cancel.png (renamed from res/drawable-hdpi/navigation_cancel.png)bin1358 -> 1358 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_cancel_dark.png (renamed from res/drawable-hdpi/navigation_cancel_dark.png)bin1285 -> 1285 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_chapters.png (renamed from res/drawable-hdpi/navigation_chapters.png)bin1979 -> 1979 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_chapters_dark.png (renamed from res/drawable-hdpi/navigation_chapters_dark.png)bin1821 -> 1821 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_collapse.png (renamed from res/drawable-hdpi/navigation_collapse.png)bin1425 -> 1425 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_collapse_dark.png (renamed from res/drawable-hdpi/navigation_collapse_dark.png)bin1384 -> 1384 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/navigation_expand.png (renamed from res/drawable-hdpi/navigation_expand.png)bin1444 -> 1444 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_expand_dark.png (renamed from res/drawable-hdpi/navigation_expand_dark.png)bin1405 -> 1405 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/navigation_refresh.png (renamed from res/drawable-hdpi/navigation_refresh.png)bin3171 -> 3171 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_refresh_dark.png (renamed from res/drawable-hdpi/navigation_refresh_dark.png)bin3138 -> 3138 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_shownotes.png (renamed from res/drawable-hdpi/navigation_shownotes.png)bin1363 -> 1363 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_shownotes_dark.png (renamed from res/drawable-hdpi/navigation_shownotes_dark.png)bin1386 -> 1386 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_up.png (renamed from res/drawable-hdpi/navigation_up.png)bin2270 -> 2270 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/navigation_up_dark.png (renamed from res/drawable-hdpi/navigation_up_dark.png)bin2221 -> 2221 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/social_share.png (renamed from res/drawable-hdpi/social_share.png)bin1695 -> 1695 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/social_share_dark.png (renamed from res/drawable-hdpi/social_share_dark.png)bin1606 -> 1606 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/spinner_button.9.png (renamed from res/drawable-hdpi/spinner_button.9.png)bin318 -> 318 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/spinner_button_dark.9.png (renamed from res/drawable-hdpi/spinner_button_dark.9.png)bin316 -> 316 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/stat_notify_sync.png (renamed from res/drawable-hdpi/stat_notify_sync.png)bin674 -> 674 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/stat_notify_sync_error.png (renamed from res/drawable-hdpi/stat_notify_sync_error.png)bin708 -> 708 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/stat_playlist.png (renamed from res/drawable-hdpi/stat_playlist.png)bin412 -> 412 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/stat_playlist_dark.png (renamed from res/drawable-hdpi/stat_playlist_dark.png)bin338 -> 338 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/type_audio.png (renamed from res/drawable-hdpi/type_audio.png)bin1983 -> 1983 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/type_audio_dark.png (renamed from res/drawable-hdpi/type_audio_dark.png)bin2008 -> 2008 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/type_video.png (renamed from res/drawable-hdpi/type_video.png)bin1215 -> 1215 bytes
-rwxr-xr-xcore/src/main/res/drawable-hdpi/type_video_dark.png (renamed from res/drawable-hdpi/type_video_dark.png)bin1211 -> 1211 bytes
-rw-r--r--core/src/main/res/drawable-ldpi-v11/ic_stat_antenna_default.png (renamed from res/drawable-ldpi-v11/ic_stat_antenna.png)bin307 -> 307 bytes
-rw-r--r--core/src/main/res/drawable-ldpi/action_stream.png (renamed from res/drawable-ldpi/action_stream.png)bin367 -> 367 bytes
-rw-r--r--core/src/main/res/drawable-ldpi/action_stream_dark.png (renamed from res/drawable-ldpi/action_stream_dark.png)bin307 -> 307 bytes
-rw-r--r--core/src/main/res/drawable-ldpi/ic_launcher.png (renamed from res/drawable-ldpi/ic_launcher.png)bin1658 -> 1658 bytes
-rw-r--r--core/src/main/res/drawable-ldpi/ic_stat_antenna_default.png (renamed from res/drawable-ldpi/ic_stat_antenna.png)bin271 -> 271 bytes
-rw-r--r--core/src/main/res/drawable-ldpi/stat_playlist.png (renamed from res/drawable-ldpi/stat_playlist.png)bin239 -> 239 bytes
-rw-r--r--core/src/main/res/drawable-ldpi/stat_playlist_dark.png (renamed from res/drawable-ldpi/stat_playlist_dark.png)bin219 -> 219 bytes
-rw-r--r--core/src/main/res/drawable-mdpi-v11/ic_stat_antenna_default.png (renamed from res/drawable-mdpi-v11/ic_stat_antenna.png)bin414 -> 414 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png (renamed from res/drawable-mdpi-v11/ic_stat_authentication.png)bin293 -> 293 bytes
-rw-r--r--core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png (renamed from res/drawable-mdpi-v11/stat_notify_sync.png)bin732 -> 732 bytes
-rw-r--r--core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png (renamed from res/drawable-mdpi-v11/stat_notify_sync_error.png)bin746 -> 746 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/action_about.png (renamed from res/drawable-mdpi/action_about.png)bin1441 -> 1441 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/action_about_dark.png (renamed from res/drawable-mdpi/action_about_dark.png)bin1333 -> 1333 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/action_search.png (renamed from res/drawable-mdpi/action_search.png)bin1429 -> 1429 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/action_search_dark.png (renamed from res/drawable-mdpi/action_search_dark.png)bin1394 -> 1394 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/action_settings.png (renamed from res/drawable-mdpi/action_settings.png)bin1358 -> 1358 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/action_settings_dark.png (renamed from res/drawable-mdpi/action_settings_dark.png)bin1339 -> 1339 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/action_stream.png (renamed from res/drawable-mdpi/action_stream.png)bin506 -> 506 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/action_stream_dark.png (renamed from res/drawable-mdpi/action_stream_dark.png)bin426 -> 426 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/av_download.png (renamed from res/drawable-mdpi/av_download.png)bin1230 -> 1230 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/av_download_dark.png (renamed from res/drawable-mdpi/av_download_dark.png)bin1238 -> 1238 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/av_fast_forward.png (renamed from res/drawable-mdpi/av_fast_forward.png)bin1277 -> 1277 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/av_fast_forward_dark.png (renamed from res/drawable-mdpi/av_fast_forward_dark.png)bin1285 -> 1285 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/av_pause.png (renamed from res/drawable-mdpi/av_pause.png)bin1109 -> 1109 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/av_pause_dark.png (renamed from res/drawable-mdpi/av_pause_dark.png)bin1107 -> 1107 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/av_play.png (renamed from res/drawable-mdpi/av_play.png)bin1261 -> 1261 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/av_play_dark.png (renamed from res/drawable-mdpi/av_play_dark.png)bin1248 -> 1248 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/av_rewind.png (renamed from res/drawable-mdpi/av_rewind.png)bin1277 -> 1277 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/av_rewind_dark.png (renamed from res/drawable-mdpi/av_rewind_dark.png)bin1277 -> 1277 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/content_discard.png (renamed from res/drawable-mdpi/content_discard.png)bin1359 -> 1359 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/content_discard_dark.png (renamed from res/drawable-mdpi/content_discard_dark.png)bin1358 -> 1358 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/content_new.png (renamed from res/drawable-mdpi/content_new.png)bin1099 -> 1099 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/content_new_dark.png (renamed from res/drawable-mdpi/content_new_dark.png)bin1090 -> 1090 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/default_cover.png (renamed from res/drawable-mdpi/default_cover.png)bin1246 -> 1246 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/default_cover_dark.png (renamed from res/drawable-mdpi/default_cover_dark.png)bin1240 -> 1240 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/device_access_time.png (renamed from res/drawable-mdpi/device_access_time.png)bin1493 -> 1493 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/device_access_time_dark.png (renamed from res/drawable-mdpi/device_access_time_dark.png)bin1408 -> 1408 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_action_overflow.png (renamed from res/drawable-mdpi/ic_action_overflow.png)bin197 -> 197 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_action_overflow_dark.png (renamed from res/drawable-mdpi/ic_action_overflow_dark.png)bin201 -> 201 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/ic_action_pause_over_video.png (renamed from res/drawable-mdpi/ic_action_pause_over_video.png)bin3233 -> 3233 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/ic_action_play_over_video.png (renamed from res/drawable-mdpi/ic_action_play_over_video.png)bin3510 -> 3510 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/ic_drag_handle.png (renamed from res/drawable-mdpi/ic_drag_handle.png)bin175 -> 175 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/ic_drag_handle_dark.png (renamed from res/drawable-mdpi/ic_drag_handle_dark.png)bin159 -> 159 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_drawer.png (renamed from res/drawable-mdpi/ic_drawer.png)bin2820 -> 2820 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_drawer_dark.png (renamed from res/drawable-mdpi/ic_drawer_dark.png)bin2816 -> 2816 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_launcher.png (renamed from res/drawable-mdpi/ic_launcher.png)bin2382 -> 2382 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/ic_new.png (renamed from res/drawable-mdpi/ic_new.png)bin593 -> 593 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/ic_new_dark.png (renamed from res/drawable-mdpi/ic_new_dark.png)bin484 -> 484 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_stat_antenna_default.png (renamed from res/drawable-mdpi/ic_stat_antenna.png)bin412 -> 412 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/ic_stat_authentication.png (renamed from res/drawable-mdpi/ic_stat_authentication.png)bin460 -> 460 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/location_web_site.png (renamed from res/drawable-mdpi/location_web_site.png)bin1827 -> 1827 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/location_web_site_dark.png (renamed from res/drawable-mdpi/location_web_site_dark.png)bin1842 -> 1842 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/navigation_accept.png (renamed from res/drawable-mdpi/navigation_accept.png)bin1197 -> 1197 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_accept_dark.png (renamed from res/drawable-mdpi/navigation_accept_dark.png)bin1191 -> 1191 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/navigation_cancel.png (renamed from res/drawable-mdpi/navigation_cancel.png)bin1202 -> 1202 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_cancel_dark.png (renamed from res/drawable-mdpi/navigation_cancel_dark.png)bin1138 -> 1138 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_chapters.png (renamed from res/drawable-mdpi/navigation_chapters.png)bin1584 -> 1584 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_chapters_dark.png (renamed from res/drawable-mdpi/navigation_chapters_dark.png)bin1453 -> 1453 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_collapse.png (renamed from res/drawable-mdpi/navigation_collapse.png)bin1238 -> 1238 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_collapse_dark.png (renamed from res/drawable-mdpi/navigation_collapse_dark.png)bin1208 -> 1208 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/navigation_expand.png (renamed from res/drawable-mdpi/navigation_expand.png)bin1242 -> 1242 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_expand_dark.png (renamed from res/drawable-mdpi/navigation_expand_dark.png)bin1214 -> 1214 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/navigation_refresh.png (renamed from res/drawable-mdpi/navigation_refresh.png)bin3058 -> 3058 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_refresh_dark.png (renamed from res/drawable-mdpi/navigation_refresh_dark.png)bin3033 -> 3033 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_shownotes.png (renamed from res/drawable-mdpi/navigation_shownotes.png)bin1254 -> 1254 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_shownotes_dark.png (renamed from res/drawable-mdpi/navigation_shownotes_dark.png)bin1253 -> 1253 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_up.png (renamed from res/drawable-mdpi/navigation_up.png)bin2123 -> 2123 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/navigation_up_dark.png (renamed from res/drawable-mdpi/navigation_up_dark.png)bin2060 -> 2060 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/social_share.png (renamed from res/drawable-mdpi/social_share.png)bin1394 -> 1394 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/social_share_dark.png (renamed from res/drawable-mdpi/social_share_dark.png)bin1341 -> 1341 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/spinner_button.9.png (renamed from res/drawable-mdpi/spinner_button.9.png)bin266 -> 266 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/spinner_button_dark.9.png (renamed from res/drawable-mdpi/spinner_button_dark.9.png)bin266 -> 266 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/stat_notify_sync.png (renamed from res/drawable-mdpi/stat_notify_sync.png)bin628 -> 628 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/stat_notify_sync_error.png (renamed from res/drawable-mdpi/stat_notify_sync_error.png)bin627 -> 627 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/stat_playlist.png (renamed from res/drawable-mdpi/stat_playlist.png)bin327 -> 327 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/stat_playlist_dark.png (renamed from res/drawable-mdpi/stat_playlist_dark.png)bin271 -> 271 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/type_audio.png (renamed from res/drawable-mdpi/type_audio.png)bin1580 -> 1580 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/type_audio_dark.png (renamed from res/drawable-mdpi/type_audio_dark.png)bin1582 -> 1582 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/type_video.png (renamed from res/drawable-mdpi/type_video.png)bin1129 -> 1129 bytes
-rwxr-xr-xcore/src/main/res/drawable-mdpi/type_video_dark.png (renamed from res/drawable-mdpi/type_video_dark.png)bin1129 -> 1129 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna_default.png (renamed from res/drawable-xhdpi-v11/ic_stat_antenna.png)bin1005 -> 1005 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png (renamed from res/drawable-xhdpi-v11/ic_stat_authentication.png)bin529 -> 529 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png (renamed from res/drawable-xhdpi-v11/stat_notify_sync.png)bin1306 -> 1306 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png (renamed from res/drawable-xhdpi-v11/stat_notify_sync_error.png)bin1434 -> 1434 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/action_about.png (renamed from res/drawable-xhdpi/action_about.png)bin2257 -> 2257 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/action_about_dark.png (renamed from res/drawable-xhdpi/action_about_dark.png)bin2040 -> 2040 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/action_search.png (renamed from res/drawable-xhdpi/action_search.png)bin2117 -> 2117 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/action_search_dark.png (renamed from res/drawable-xhdpi/action_search_dark.png)bin2127 -> 2127 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/action_settings.png (renamed from res/drawable-xhdpi/action_settings.png)bin1671 -> 1671 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/action_settings_dark.png (renamed from res/drawable-xhdpi/action_settings_dark.png)bin1641 -> 1641 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/action_stream.png (renamed from res/drawable-xhdpi/action_stream.png)bin1099 -> 1099 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/action_stream_dark.png (renamed from res/drawable-xhdpi/action_stream_dark.png)bin974 -> 974 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/av_download.png (renamed from res/drawable-xhdpi/av_download.png)bin1473 -> 1473 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/av_download_dark.png (renamed from res/drawable-xhdpi/av_download_dark.png)bin1482 -> 1482 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/av_fast_forward.png (renamed from res/drawable-xhdpi/av_fast_forward.png)bin1668 -> 1668 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/av_fast_forward_dark.png (renamed from res/drawable-xhdpi/av_fast_forward_dark.png)bin1664 -> 1664 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/av_pause.png (renamed from res/drawable-xhdpi/av_pause.png)bin1159 -> 1159 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/av_pause_dark.png (renamed from res/drawable-xhdpi/av_pause_dark.png)bin1181 -> 1181 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/av_play.png (renamed from res/drawable-xhdpi/av_play.png)bin1578 -> 1578 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/av_play_dark.png (renamed from res/drawable-xhdpi/av_play_dark.png)bin1620 -> 1620 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/av_rewind.png (renamed from res/drawable-xhdpi/av_rewind.png)bin1659 -> 1659 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/av_rewind_dark.png (renamed from res/drawable-xhdpi/av_rewind_dark.png)bin1694 -> 1694 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/content_discard.png (renamed from res/drawable-xhdpi/content_discard.png)bin1848 -> 1848 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/content_discard_dark.png (renamed from res/drawable-xhdpi/content_discard_dark.png)bin1824 -> 1824 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/content_new.png (renamed from res/drawable-xhdpi/content_new.png)bin1225 -> 1225 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/content_new_dark.png (renamed from res/drawable-xhdpi/content_new_dark.png)bin1221 -> 1221 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/content_remove.png (renamed from res/drawable-xhdpi/content_remove.png)bin1488 -> 1488 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/content_remove_dark.png (renamed from res/drawable-xhdpi/content_remove_dark.png)bin1348 -> 1348 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/default_cover.png (renamed from res/drawable-xhdpi/default_cover.png)bin1522 -> 1522 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/default_cover_dark.png (renamed from res/drawable-xhdpi/default_cover_dark.png)bin1544 -> 1544 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/device_access_time.png (renamed from res/drawable-xhdpi/device_access_time.png)bin2423 -> 2423 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/device_access_time_dark.png (renamed from res/drawable-xhdpi/device_access_time_dark.png)bin2284 -> 2284 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_action_overflow.png (renamed from res/drawable-xhdpi/ic_action_overflow.png)bin267 -> 267 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png (renamed from res/drawable-xhdpi/ic_action_overflow_dark.png)bin262 -> 262 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png (renamed from res/drawable-xhdpi/ic_action_pause_over_video.png)bin10241 -> 10241 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/ic_action_play_over_video.png (renamed from res/drawable-xhdpi/ic_action_play_over_video.png)bin11175 -> 11175 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/ic_drag_handle.png (renamed from res/drawable-xhdpi/ic_drag_handle.png)bin234 -> 234 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png (renamed from res/drawable-xhdpi/ic_drag_handle_dark.png)bin216 -> 216 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_drawer.png (renamed from res/drawable-xhdpi/ic_drawer.png)bin2836 -> 2836 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_drawer_dark.png (renamed from res/drawable-xhdpi/ic_drawer_dark.png)bin1038 -> 1038 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_launcher.png (renamed from res/drawable-xhdpi/ic_launcher.png)bin5589 -> 5589 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/ic_new.png (renamed from res/drawable-xhdpi/ic_new.png)bin1189 -> 1189 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/ic_new_dark.png (renamed from res/drawable-xhdpi/ic_new_dark.png)bin989 -> 989 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_stat_antenna_default.png (renamed from res/drawable-xhdpi/ic_stat_antenna.png)bin942 -> 942 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/ic_stat_authentication.png (renamed from res/drawable-xhdpi/ic_stat_authentication.png)bin882 -> 882 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_undobar_undo.png (renamed from res/drawable-xhdpi/ic_undobar_undo.png)bin1558 -> 1558 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/location_web_site.png (renamed from res/drawable-xhdpi/location_web_site.png)bin3291 -> 3291 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/location_web_site_dark.png (renamed from res/drawable-xhdpi/location_web_site_dark.png)bin3307 -> 3307 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/navigation_accept.png (renamed from res/drawable-xhdpi/navigation_accept.png)bin1546 -> 1546 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_accept_dark.png (renamed from res/drawable-xhdpi/navigation_accept_dark.png)bin1599 -> 1599 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/navigation_cancel.png (renamed from res/drawable-xhdpi/navigation_cancel.png)bin1488 -> 1488 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_cancel_dark.png (renamed from res/drawable-xhdpi/navigation_cancel_dark.png)bin1348 -> 1348 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_chapters.png (renamed from res/drawable-xhdpi/navigation_chapters.png)bin2524 -> 2524 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_chapters_dark.png (renamed from res/drawable-xhdpi/navigation_chapters_dark.png)bin2366 -> 2366 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_collapse.png (renamed from res/drawable-xhdpi/navigation_collapse.png)bin1658 -> 1658 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_collapse_dark.png (renamed from res/drawable-xhdpi/navigation_collapse_dark.png)bin1635 -> 1635 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/navigation_expand.png (renamed from res/drawable-xhdpi/navigation_expand.png)bin1702 -> 1702 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_expand_dark.png (renamed from res/drawable-xhdpi/navigation_expand_dark.png)bin1677 -> 1677 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/navigation_refresh.png (renamed from res/drawable-xhdpi/navigation_refresh.png)bin3272 -> 3272 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_refresh_dark.png (renamed from res/drawable-xhdpi/navigation_refresh_dark.png)bin3219 -> 3219 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_shownotes.png (renamed from res/drawable-xhdpi/navigation_shownotes.png)bin1414 -> 1414 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png (renamed from res/drawable-xhdpi/navigation_shownotes_dark.png)bin1446 -> 1446 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_up.png (renamed from res/drawable-xhdpi/navigation_up.png)bin2471 -> 2471 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/navigation_up_dark.png (renamed from res/drawable-xhdpi/navigation_up_dark.png)bin2445 -> 2445 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/social_share.png (renamed from res/drawable-xhdpi/social_share.png)bin1989 -> 1989 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/social_share_dark.png (renamed from res/drawable-xhdpi/social_share_dark.png)bin1780 -> 1780 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/spinner_button.9.png (renamed from res/drawable-xhdpi/spinner_button.9.png)bin405 -> 405 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/spinner_button_dark.9.png (renamed from res/drawable-xhdpi/spinner_button_dark.9.png)bin406 -> 406 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/stat_playlist.png (renamed from res/drawable-xhdpi/stat_playlist.png)bin494 -> 494 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/stat_playlist_dark.png (renamed from res/drawable-xhdpi/stat_playlist_dark.png)bin440 -> 440 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/type_audio.png (renamed from res/drawable-xhdpi/type_audio.png)bin2437 -> 2437 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/type_audio_dark.png (renamed from res/drawable-xhdpi/type_audio_dark.png)bin2489 -> 2489 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/type_video.png (renamed from res/drawable-xhdpi/type_video.png)bin1327 -> 1327 bytes
-rwxr-xr-xcore/src/main/res/drawable-xhdpi/type_video_dark.png (renamed from res/drawable-xhdpi/type_video_dark.png)bin1337 -> 1337 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/undobar.9.png (renamed from res/drawable-xhdpi/undobar.9.png)bin1665 -> 1665 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png (renamed from res/drawable-xhdpi/undobar_button_focused.9.png)bin1141 -> 1141 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png (renamed from res/drawable-xhdpi/undobar_button_pressed.9.png)bin1123 -> 1123 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/undobar_divider.9.png (renamed from res/drawable-xhdpi/undobar_divider.9.png)bin963 -> 963 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_action_overflow.png (renamed from res/drawable-xxhdpi/ic_action_overflow.png)bin264 -> 264 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png (renamed from res/drawable-xxhdpi/ic_action_overflow_dark.png)bin264 -> 264 bytes
-rwxr-xr-xcore/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png (renamed from res/drawable-xxhdpi/ic_action_pause_over_video.png)bin21550 -> 21550 bytes
-rwxr-xr-xcore/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png (renamed from res/drawable-xxhdpi/ic_action_play_over_video.png)bin23322 -> 23322 bytes
-rwxr-xr-xcore/src/main/res/drawable-xxhdpi/ic_drag_handle.png (renamed from res/drawable-xxhdpi/ic_drag_handle.png)bin290 -> 290 bytes
-rwxr-xr-xcore/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png (renamed from res/drawable-xxhdpi/ic_drag_handle_dark.png)bin265 -> 265 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_drawer.png (renamed from res/drawable-xxhdpi/ic_drawer.png)bin202 -> 202 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_drawer_dark.png (renamed from res/drawable-xxhdpi/ic_drawer_dark.png)bin202 -> 202 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_launcher.png (renamed from res/drawable-xxhdpi/ic_launcher.png)bin14262 -> 14262 bytes
-rwxr-xr-xcore/src/main/res/drawable-xxhdpi/ic_new.png (renamed from res/drawable-xxhdpi/ic_new.png)bin1759 -> 1759 bytes
-rwxr-xr-xcore/src/main/res/drawable-xxhdpi/ic_new_dark.png (renamed from res/drawable-xxhdpi/ic_new_dark.png)bin1501 -> 1501 bytes
-rwxr-xr-xcore/src/main/res/drawable-xxhdpi/ic_stat_authentication.png (renamed from res/drawable-xxhdpi/ic_stat_authentication.png)bin1266 -> 1266 bytes
-rw-r--r--core/src/main/res/drawable/badge.xml (renamed from res/drawable/badge.xml)0
-rw-r--r--core/src/main/res/drawable/borderless_button.xml (renamed from res/drawable/borderless_button.xml)0
-rw-r--r--core/src/main/res/drawable/borderless_button_dark.xml (renamed from res/drawable/borderless_button_dark.xml)0
-rw-r--r--core/src/main/res/drawable/horizontal_divider.9.png (renamed from res/drawable/horizontal_divider.9.png)bin159 -> 159 bytes
-rw-r--r--core/src/main/res/drawable/overlay_button_circle_background.xml (renamed from res/drawable/overlay_button_circle_background.xml)0
-rw-r--r--core/src/main/res/drawable/overlay_drawable.xml (renamed from res/drawable/overlay_drawable.xml)0
-rw-r--r--core/src/main/res/drawable/overlay_drawable_dark.xml (renamed from res/drawable/overlay_drawable_dark.xml)0
-rw-r--r--core/src/main/res/drawable/type_audio.png (renamed from res/drawable/type_audio.png)bin1580 -> 1580 bytes
-rw-r--r--core/src/main/res/drawable/type_video.png (renamed from res/drawable/type_video.png)bin1129 -> 1129 bytes
-rw-r--r--core/src/main/res/drawable/undobar_button.xml (renamed from res/drawable/undobar_button.xml)0
-rw-r--r--core/src/main/res/drawable/vertical_divider.9.png (renamed from res/drawable/vertical_divider.9.png)bin191 -> 191 bytes
-rw-r--r--core/src/main/res/drawable/white_circle.xml (renamed from res/drawable/white_circle.xml)0
-rw-r--r--core/src/main/res/values-az/strings.xml (renamed from res/values-az/strings.xml)0
-rw-r--r--core/src/main/res/values-ca/strings.xml (renamed from res/values-ca/strings.xml)0
-rw-r--r--core/src/main/res/values-cs-rCZ/strings.xml (renamed from res/values-cs-rCZ/strings.xml)0
-rw-r--r--core/src/main/res/values-da/strings.xml (renamed from res/values-da/strings.xml)0
-rw-r--r--core/src/main/res/values-de/strings.xml (renamed from res/values-de/strings.xml)0
-rw-r--r--core/src/main/res/values-es-rES/strings.xml (renamed from res/values-es-rES/strings.xml)0
-rw-r--r--core/src/main/res/values-es/strings.xml (renamed from res/values-es/strings.xml)0
-rw-r--r--core/src/main/res/values-fr/strings.xml (renamed from res/values-fr/strings.xml)0
-rw-r--r--core/src/main/res/values-hi-rIN/strings.xml (renamed from res/values-hi-rIN/strings.xml)0
-rw-r--r--core/src/main/res/values-it-rIT/strings.xml (renamed from res/values-it-rIT/strings.xml)0
-rw-r--r--core/src/main/res/values-iw-rIL/strings.xml (renamed from res/values-iw-rIL/strings.xml)0
-rw-r--r--core/src/main/res/values-ko/strings.xml (renamed from res/values-ko/strings.xml)0
-rw-r--r--core/src/main/res/values-land/styles.xml (renamed from res/values-land/styles.xml)0
-rw-r--r--core/src/main/res/values-large/dimens.xml (renamed from res/values-large/dimens.xml)0
-rw-r--r--core/src/main/res/values-nl/strings.xml (renamed from res/values-nl/strings.xml)0
-rw-r--r--core/src/main/res/values-pl-rPL/strings.xml (renamed from res/values-pl-rPL/strings.xml)0
-rw-r--r--core/src/main/res/values-pt-rBR/strings.xml (renamed from res/values-pt-rBR/strings.xml)0
-rw-r--r--core/src/main/res/values-pt/strings.xml (renamed from res/values-pt/strings.xml)0
-rw-r--r--core/src/main/res/values-ro-rRO/strings.xml (renamed from res/values-ro-rRO/strings.xml)0
-rw-r--r--core/src/main/res/values-ru/strings.xml (renamed from res/values-ru/strings.xml)0
-rw-r--r--core/src/main/res/values-sv-rSE/strings.xml (renamed from res/values-sv-rSE/strings.xml)0
-rw-r--r--core/src/main/res/values-uk-rUA/strings.xml (renamed from res/values-uk-rUA/strings.xml)0
-rw-r--r--core/src/main/res/values-v11/colors.xml (renamed from res/values-v11/colors.xml)0
-rw-r--r--core/src/main/res/values-v14/dimens.xml (renamed from res/values-v14/dimens.xml)0
-rw-r--r--core/src/main/res/values-v14/styles.xml (renamed from res/values-v14/styles.xml)0
-rw-r--r--core/src/main/res/values-v16/styles.xml (renamed from res/values-v16/styles.xml)0
-rw-r--r--core/src/main/res/values-v19/colors.xml (renamed from res/values-v19/colors.xml)0
-rw-r--r--core/src/main/res/values-zh-rCN/strings.xml (renamed from res/values-zh-rCN/strings.xml)0
-rw-r--r--core/src/main/res/values/arrays.xml (renamed from res/values/arrays.xml)0
-rw-r--r--core/src/main/res/values/attrs.xml (renamed from res/values/attrs.xml)0
-rw-r--r--core/src/main/res/values/colors.xml (renamed from res/values/colors.xml)0
-rw-r--r--core/src/main/res/values/dimens.xml (renamed from res/values/dimens.xml)0
-rw-r--r--core/src/main/res/values/ids.xml (renamed from res/values/ids.xml)0
-rw-r--r--core/src/main/res/values/integers.xml (renamed from res/values/integers.xml)0
-rw-r--r--core/src/main/res/values/strings.xml (renamed from res/values/strings.xml)0
-rw-r--r--core/src/main/res/values/styles.xml (renamed from res/values/styles.xml)0
-rw-r--r--gradle/wrapper/gradle-wrapper.properties4
-rw-r--r--pom.xml269
-rw-r--r--proguard-mvn.cfg66
-rw-r--r--proguard-project.txt20
-rw-r--r--settings.gradle3
-rw-r--r--src/com/aocate/media/MediaPlayer.java1296
-rw-r--r--src/com/aocate/media/ServiceBackedMediaPlayer.java1201
-rw-r--r--src/de/danoeh/antennapod/PodcastApp.java47
-rw-r--r--src/de/danoeh/antennapod/activity/AudioplayerActivity.java746
-rw-r--r--src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java248
-rw-r--r--src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java370
-rw-r--r--src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java110
-rw-r--r--src/de/danoeh/antennapod/activity/FeedInfoActivity.java192
-rw-r--r--src/de/danoeh/antennapod/activity/FlattrAuthActivity.java125
-rw-r--r--src/de/danoeh/antennapod/activity/MainActivity.java432
-rw-r--r--src/de/danoeh/antennapod/activity/MediaplayerActivity.java525
-rw-r--r--src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java428
-rw-r--r--src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java134
-rw-r--r--src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java90
-rw-r--r--src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java38
-rw-r--r--src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java172
-rw-r--r--src/de/danoeh/antennapod/activity/OpmlImportHolder.java29
-rw-r--r--src/de/danoeh/antennapod/activity/PreferenceActivity.java532
-rw-r--r--src/de/danoeh/antennapod/activity/StorageErrorActivity.java75
-rw-r--r--src/de/danoeh/antennapod/activity/VideoplayerActivity.java359
-rw-r--r--src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java372
-rw-r--r--src/de/danoeh/antennapod/adapter/ActionButtonCallback.java8
-rw-r--r--src/de/danoeh/antennapod/adapter/ActionButtonUtils.java78
-rw-r--r--src/de/danoeh/antennapod/adapter/AdapterUtils.java57
-rw-r--r--src/de/danoeh/antennapod/adapter/ChapterListAdapter.java180
-rw-r--r--src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java57
-rw-r--r--src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java112
-rw-r--r--src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java122
-rw-r--r--src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java142
-rw-r--r--src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java306
-rw-r--r--src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java220
-rw-r--r--src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java55
-rw-r--r--src/de/danoeh/antennapod/adapter/NavListAdapter.java229
-rw-r--r--src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java170
-rw-r--r--src/de/danoeh/antennapod/adapter/QueueListAdapter.java127
-rw-r--r--src/de/danoeh/antennapod/adapter/SearchlistAdapter.java110
-rw-r--r--src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java68
-rw-r--r--src/de/danoeh/antennapod/asynctask/DownloadObserver.java177
-rw-r--r--src/de/danoeh/antennapod/asynctask/FeedRemover.java74
-rw-r--r--src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java238
-rw-r--r--src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java47
-rw-r--r--src/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java95
-rw-r--r--src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java114
-rw-r--r--src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java69
-rw-r--r--src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java116
-rw-r--r--src/de/danoeh/antennapod/asynctask/PicassoImageResource.java37
-rw-r--r--src/de/danoeh/antennapod/asynctask/PicassoProvider.java152
-rw-r--r--src/de/danoeh/antennapod/backup/OpmlBackupAgent.java212
-rw-r--r--src/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java107
-rw-r--r--src/de/danoeh/antennapod/dialog/ConfirmationDialog.java64
-rw-r--r--src/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java30
-rw-r--r--src/de/danoeh/antennapod/dialog/FeedItemDialog.java434
-rw-r--r--src/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java67
-rw-r--r--src/de/danoeh/antennapod/dialog/TimeDialog.java138
-rw-r--r--src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java100
-rw-r--r--src/de/danoeh/antennapod/feed/Chapter.java55
-rw-r--r--src/de/danoeh/antennapod/feed/EventDistributor.java140
-rw-r--r--src/de/danoeh/antennapod/feed/Feed.java445
-rw-r--r--src/de/danoeh/antennapod/feed/FeedComponent.java66
-rw-r--r--src/de/danoeh/antennapod/feed/FeedFile.java105
-rw-r--r--src/de/danoeh/antennapod/feed/FeedImage.java77
-rw-r--r--src/de/danoeh/antennapod/feed/FeedItem.java333
-rw-r--r--src/de/danoeh/antennapod/feed/FeedMedia.java411
-rw-r--r--src/de/danoeh/antennapod/feed/FeedPreferences.java89
-rw-r--r--src/de/danoeh/antennapod/feed/ID3Chapter.java36
-rw-r--r--src/de/danoeh/antennapod/feed/MediaType.java5
-rw-r--r--src/de/danoeh/antennapod/feed/SearchResult.java34
-rw-r--r--src/de/danoeh/antennapod/feed/SimpleChapter.java25
-rw-r--r--src/de/danoeh/antennapod/feed/VorbisCommentChapter.java109
-rw-r--r--src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java196
-rw-r--r--src/de/danoeh/antennapod/fragment/CoverFragment.java105
-rw-r--r--src/de/danoeh/antennapod/fragment/DownloadLogFragment.java121
-rw-r--r--src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java238
-rw-r--r--src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java476
-rw-r--r--src/de/danoeh/antennapod/fragment/ItemlistFragment.java456
-rw-r--r--src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java425
-rw-r--r--src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java288
-rw-r--r--src/de/danoeh/antennapod/fragment/QueueFragment.java383
-rw-r--r--src/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java69
-rw-r--r--src/de/danoeh/antennapod/fragment/SearchFragment.java258
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java169
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java20
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java80
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java26
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java50
-rw-r--r--src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java146
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetService.java718
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java21
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java12
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java19
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java72
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java65
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java41
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java46
-rw-r--r--src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java56
-rw-r--r--src/de/danoeh/antennapod/opml/OpmlElement.java46
-rw-r--r--src/de/danoeh/antennapod/opml/OpmlReader.java87
-rw-r--r--src/de/danoeh/antennapod/opml/OpmlSymbols.java21
-rw-r--r--src/de/danoeh/antennapod/opml/OpmlWriter.java65
-rw-r--r--src/de/danoeh/antennapod/preferences/GpodnetPreferences.java246
-rw-r--r--src/de/danoeh/antennapod/preferences/PlaybackPreferences.java146
-rw-r--r--src/de/danoeh/antennapod/preferences/UserPreferences.java609
-rw-r--r--src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java33
-rw-r--r--src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java46
-rw-r--r--src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java46
-rw-r--r--src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java32
-rw-r--r--src/de/danoeh/antennapod/receiver/PlayerWidget.java50
-rw-r--r--src/de/danoeh/antennapod/receiver/SPAReceiver.java55
-rw-r--r--src/de/danoeh/antennapod/service/GpodnetSyncService.java245
-rw-r--r--src/de/danoeh/antennapod/service/download/APRedirectHandler.java54
-rw-r--r--src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java96
-rw-r--r--src/de/danoeh/antennapod/service/download/DownloadRequest.java209
-rw-r--r--src/de/danoeh/antennapod/service/download/DownloadService.java1230
-rw-r--r--src/de/danoeh/antennapod/service/download/DownloadStatus.java181
-rw-r--r--src/de/danoeh/antennapod/service/download/Downloader.java69
-rw-r--r--src/de/danoeh/antennapod/service/download/DownloaderCallback.java10
-rw-r--r--src/de/danoeh/antennapod/service/download/HttpDownloader.java246
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackService.java1132
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java979
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java384
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlayerStatus.java14
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java201
-rw-r--r--src/de/danoeh/antennapod/storage/DBReader.java908
-rw-r--r--src/de/danoeh/antennapod/storage/DBTasks.java895
-rw-r--r--src/de/danoeh/antennapod/storage/DBWriter.java974
-rw-r--r--src/de/danoeh/antennapod/storage/DownloadRequestException.java25
-rw-r--r--src/de/danoeh/antennapod/storage/DownloadRequester.java367
-rw-r--r--src/de/danoeh/antennapod/storage/FeedItemStatistics.java70
-rw-r--r--src/de/danoeh/antennapod/storage/FeedSearcher.java57
-rw-r--r--src/de/danoeh/antennapod/storage/PodDBAdapter.java1391
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/FeedHandler.java34
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java19
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/HandlerState.java98
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/SyndHandler.java126
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/TypeGetter.java112
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java38
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/NSContent.java25
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/NSITunes.java51
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/NSMedia.java68
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java141
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java51
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/Namespace.java21
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/SyndElement.java22
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java46
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java194
-rw-r--r--src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java161
-rw-r--r--src/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java42
-rw-r--r--src/de/danoeh/antennapod/util/ChapterUtils.java274
-rw-r--r--src/de/danoeh/antennapod/util/Converter.java103
-rw-r--r--src/de/danoeh/antennapod/util/DownloadError.java52
-rw-r--r--src/de/danoeh/antennapod/util/DuckType.java117
-rw-r--r--src/de/danoeh/antennapod/util/EpisodeFilter.java49
-rw-r--r--src/de/danoeh/antennapod/util/FeedtitleComparator.java15
-rw-r--r--src/de/danoeh/antennapod/util/FileNameGenerator.java36
-rw-r--r--src/de/danoeh/antennapod/util/InvalidFeedException.java21
-rw-r--r--src/de/danoeh/antennapod/util/LangUtils.java120
-rw-r--r--src/de/danoeh/antennapod/util/NetworkUtils.java69
-rw-r--r--src/de/danoeh/antennapod/util/QueueAccess.java93
-rw-r--r--src/de/danoeh/antennapod/util/ShareUtils.java34
-rw-r--r--src/de/danoeh/antennapod/util/ShownotesProvider.java16
-rw-r--r--src/de/danoeh/antennapod/util/StorageUtils.java66
-rw-r--r--src/de/danoeh/antennapod/util/ThemeUtils.java22
-rw-r--r--src/de/danoeh/antennapod/util/URIUtil.java35
-rw-r--r--src/de/danoeh/antennapod/util/URLChecker.java54
-rw-r--r--src/de/danoeh/antennapod/util/UndoBarController.java137
-rw-r--r--src/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java20
-rw-r--r--src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java15
-rw-r--r--src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java19
-rw-r--r--src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java19
-rw-r--r--src/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java14
-rw-r--r--src/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java23
-rw-r--r--src/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java25
-rw-r--r--src/de/danoeh/antennapod/util/flattr/FlattrStatus.java68
-rw-r--r--src/de/danoeh/antennapod/util/flattr/FlattrThing.java7
-rw-r--r--src/de/danoeh/antennapod/util/flattr/FlattrUtils.java305
-rw-r--r--src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java30
-rw-r--r--src/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java55
-rw-r--r--src/de/danoeh/antennapod/util/id3reader/ChapterReader.java118
-rw-r--r--src/de/danoeh/antennapod/util/id3reader/ID3Reader.java250
-rw-r--r--src/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java20
-rw-r--r--src/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java17
-rw-r--r--src/de/danoeh/antennapod/util/id3reader/model/Header.java29
-rw-r--r--src/de/danoeh/antennapod/util/id3reader/model/TagHeader.java26
-rw-r--r--src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java191
-rw-r--r--src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java87
-rw-r--r--src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java31
-rw-r--r--src/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java9
-rw-r--r--src/de/danoeh/antennapod/util/playback/AudioPlayer.java34
-rw-r--r--src/de/danoeh/antennapod/util/playback/ExternalMedia.java237
-rw-r--r--src/de/danoeh/antennapod/util/playback/IPlayer.java69
-rw-r--r--src/de/danoeh/antennapod/util/playback/MediaPlayerError.java23
-rw-r--r--src/de/danoeh/antennapod/util/playback/Playable.java207
-rw-r--r--src/de/danoeh/antennapod/util/playback/PlaybackController.java784
-rw-r--r--src/de/danoeh/antennapod/util/playback/Timeline.java161
-rw-r--r--src/de/danoeh/antennapod/util/playback/VideoPlayer.java67
-rw-r--r--src/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java78
-rw-r--r--src/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java81
-rw-r--r--src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java101
-rw-r--r--src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java26
-rw-r--r--src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java194
-rw-r--r--src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java24
-rw-r--r--src/instrumentationTest/de/test/antennapod/AntennaPodTestRunner.java16
-rw-r--r--src/instrumentationTest/de/test/antennapod/gpodnet/GPodnetServiceTest.java114
-rw-r--r--src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java170
-rw-r--r--src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java1177
-rw-r--r--src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java333
-rw-r--r--src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java408
-rw-r--r--src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java326
-rw-r--r--src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java57
-rw-r--r--src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java796
-rw-r--r--src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java176
-rw-r--r--src/instrumentationTest/de/test/antennapod/ui/MainActivityTest.java117
-rw-r--r--src/instrumentationTest/de/test/antennapod/ui/PlaybackTest.java149
-rw-r--r--src/instrumentationTest/de/test/antennapod/ui/UITestUtils.java201
-rw-r--r--src/instrumentationTest/de/test/antennapod/ui/UITestUtilsTest.java94
-rw-r--r--src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java38
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/ConverterTest.java35
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/FilenameGeneratorTest.java59
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/URIUtilTest.java21
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java76
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java127
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java346
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java1420
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/syndication/FeedDiscovererTest.java109
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java118
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java28
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java21
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java110
m---------submodules/dslv0
-rw-r--r--tests/.classpath10
-rw-r--r--tests/.project33
-rw-r--r--tests/AndroidManifest.xml19
-rw-r--r--tests/lint.xml3
-rw-r--r--tests/proguard-project.txt20
-rw-r--r--tests/project.properties14
-rw-r--r--tests/res/drawable-hdpi/ic_launcher.pngbin9397 -> 0 bytes
-rw-r--r--tests/res/drawable-ldpi/ic_launcher.pngbin2729 -> 0 bytes
-rw-r--r--tests/res/drawable-mdpi/ic_launcher.pngbin5237 -> 0 bytes
-rw-r--r--tests/res/drawable-xhdpi/ic_launcher.pngbin14383 -> 0 bytes
-rw-r--r--tests/res/values/strings.xml6
896 files changed, 43317 insertions, 43158 deletions
diff --git a/.gitmodules b/.gitmodules
index 17dac28b0..2529169ca 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "submodules/dslv"]
path = submodules/dslv
url = git://github.com/danieloeh/drag-sort-listview.git
+[submodule "app/dslv"]
+ path = app/dslv
+ url = https://github.com/danieloeh/drag-sort-listview.git
diff --git a/.tx/config b/.tx/config
index e45dcd70c..ee84a774e 100644
--- a/.tx/config
+++ b/.tx/config
@@ -2,31 +2,31 @@
host = https://www.transifex.com
[antennapod.english]
-source_file = res/values/strings.xml
+source_file = core/src/main/res/values/strings.xml
source_lang = en
-trans.az = res/values-az/strings.xml
-trans.ca = res/values-ca/strings.xml
-trans.cs_CZ = res/values-cs-rCZ/strings.xml
-trans.da = res/values-da/strings.xml
-trans.de = res/values-de/strings.xml
-trans.es = res/values-es/strings.xml
-trans.es_ES = res/values-es-rES/strings.xml
-trans.fr = res/values-fr/strings.xml
-trans.he_IL = res/values-iw-rIL/strings.xml
-trans.hi_IN = res/values-hi-rIN/strings.xml
-trans.it_IT = res/values-it-rIT/strings.xml
-trans.ko = res/values-ko/strings.xml
-trans.nl = res/values-nl/strings.xml
-trans.pl_PL = res/values-pl-rPL/strings.xml
-trans.pt = res/values-pt/strings.xml
-trans.pt_BR = res/values-pt-rBR/strings.xml
-trans.ro_RO = res/values-ro-rRO/strings.xml
-trans.ru = res/values-ru/strings.xml
-trans.ru-RU = res/values-ru/strings.xml
-trans.ru_RU = res/values-ru/strings.xml
-trans.uk_UA = res/values-uk-rUA/strings.xml
-trans.zh_CN = res/values-zh-rCN/strings.xml
-trans.sv_SE = res/values-sv-rSE/strings.xml
+trans.az = core/src/main/res/values-az/strings.xml
+trans.ca = core/src/main/res/values-ca/strings.xml
+trans.cs_CZ = core/src/main/res/values-cs-rCZ/strings.xml
+trans.da = core/src/main/res/values-da/strings.xml
+trans.de = core/src/main/res/values-de/strings.xml
+trans.es = core/src/main/res/values-es/strings.xml
+trans.es_ES = core/src/main/res/values-es-rES/strings.xml
+trans.fr = core/src/main/res/values-fr/strings.xml
+trans.he_IL = core/src/main/res/values-iw-rIL/strings.xml
+trans.hi_IN = core/src/main/res/values-hi-rIN/strings.xml
+trans.it_IT = core/src/main/res/values-it-rIT/strings.xml
+trans.ko = core/src/main/res/values-ko/strings.xml
+trans.nl = core/src/main/res/values-nl/strings.xml
+trans.pl_PL = core/src/main/res/values-pl-rPL/strings.xml
+trans.pt = core/src/main/res/values-pt/strings.xml
+trans.pt_BR = core/src/main/res/values-pt-rBR/strings.xml
+trans.ro_RO = core/src/main/res/values-ro-rRO/strings.xml
+trans.ru = core/src/main/res/values-ru/strings.xml
+trans.ru-RU = core/src/main/res/values-ru/strings.xml
+trans.ru_RU = core/src/main/res/values-ru/strings.xml
+trans.uk_UA = core/src/main/res/values-uk-rUA/strings.xml
+trans.zh_CN = core/src/main/res/values-zh-rCN/strings.xml
+trans.sv_SE = core/src/main/res/values-sv-rSE/strings.xml
[antennapod.description]
file_filter = description/<lang>.txt
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
deleted file mode 100644
index ee87cc8b5..000000000
--- a/AndroidManifest.xml
+++ /dev/null
@@ -1,348 +0,0 @@
-<?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=".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"/>
-
- <service
- android:name=".service.download.DownloadService"
- android:enabled="true"/>
- <service
- android:name=".service.playback.PlaybackService"
- android:enabled="true"
- android:exported="true">
- </service>
-
- <service
- android:name=".service.GpodnetSyncService"
- android:enabled="true">
- </service>
-
- <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>
-
- <receiver
- android:name=".receiver.MediaButtonReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MEDIA_BUTTON"/>
- </intent-filter>
- <intent-filter>
- <action android:name="de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"/>
- </intent-filter>
- </receiver>
-
- <activity android:name=".activity.FeedInfoActivity">
- </activity>
-
- <service
- android:name=".service.playback.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>
- <receiver android:name=".receiver.FeedUpdateReceiver">
- <intent-filter>
- <action android:name="de.danoeh.antennapod.feedupdatereceiver.refreshFeeds"/>
- </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.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>
- <receiver android:name=".receiver.SPAReceiver">
- <intent-filter>
- <action android:name="de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"/>
- </intent-filter>
- </receiver>
- </application>
-
-</manifest>
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 000000000..d780ed9dd
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,100 @@
+apply plugin: 'com.android.application'
+
+repositories {
+ mavenCentral()
+}
+dependencies {
+ compile 'com.android.support:support-v4:20.0.0'
+ compile 'com.android.support:appcompat-v7:20.0.0'
+ compile 'org.apache.commons:commons-lang3:3.3.2'
+ compile('org.shredzone.flattr4j:flattr4j-core:2.10') {
+ exclude group: 'org.apache.httpcomponents', module: 'httpcore'
+ exclude group: 'org.apache.httpcomponents', module: 'httpclient'
+ exclude group: 'org.json', module: 'json'
+ }
+ compile 'commons-io:commons-io:2.4'
+ compile 'com.nineoldandroids:library:2.4.0'
+ compile project('dslv:library')
+ compile 'com.jayway.android.robotium:robotium-solo:5.2.1'
+ compile('com.doomonafireball.betterpickers:library:1.5.2') {
+ exclude group: 'com.android.support', module: 'support-v4'
+ }
+ compile 'org.jsoup:jsoup:1.7.3'
+ compile 'com.squareup.picasso:picasso:2.3.4'
+ compile 'com.squareup.okhttp:okhttp:2.0.0'
+ compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0'
+ compile 'com.squareup.okio:okio:1.0.0'
+ compile project(':core')
+}
+
+android {
+ compileSdkVersion 20
+ buildToolsVersion "20.0"
+
+ defaultConfig {
+ minSdkVersion 10
+ targetSdkVersion 20
+ testApplicationId "de.test.antennapod"
+ testInstrumentationRunner "de.test.antennapod.AntennaPodTestRunner"
+ }
+
+ signingConfigs {
+ releaseConfig {
+ if (project.hasProperty('releaseStoreFile')) {
+ storeFile file(releaseStoreFile)
+ } else {
+ storeFile file('keystore')
+ }
+ if (project.hasProperty('releaseStorePassword')) {
+ storePassword releaseStorePassword
+ } else {
+ storePassword "password"
+ }
+ if (project.hasProperty('releaseKeyAlias')) {
+ keyAlias releaseKeyAlias
+ } else {
+ keyAlias "alias"
+ }
+ if (project.hasProperty('releaseKeyPassword')) {
+ keyPassword releaseKeyPassword
+ } else {
+ keyPassword "password"
+ }
+ }
+ }
+
+ buildTypes {
+ def STRING = "String"
+ def FLATTR_APP_KEY = "FLATTR_APP_KEY"
+ def FLATTR_APP_SECRET = "FLATTR_APP_SECRET"
+ def mFlattrAppKey = (project.hasProperty('flattrAppKey')) ? flattrAppKey : "\"\""
+ def mFlattrAppSecret = (project.hasProperty('flattrAppSecret')) ? flattrAppSecret : "\"\""
+
+ debug {
+ applicationIdSuffix ".debug"
+ buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey
+ buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret
+ }
+ release {
+ runProguard true
+ proguardFile 'proguard.cfg'
+ signingConfig signingConfigs.releaseConfig
+ buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey
+ buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/LICENSE.txt'
+ exclude 'META-INF/NOTICE.txt'
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+} \ No newline at end of file
diff --git a/app/dslv b/app/dslv
new file mode 160000
+Subproject 80011c50e444e1c7d5e13b57bdb127b524a1ff9
diff --git a/lint.xml b/app/lint.xml
index 0d4eb14d3..0d4eb14d3 100644
--- a/lint.xml
+++ b/app/lint.xml
diff --git a/proguard.cfg b/app/proguard.cfg
index 1838f007c..1838f007c 100644
--- a/proguard.cfg
+++ b/app/proguard.cfg
diff --git a/app/settings.gradle b/app/settings.gradle
new file mode 100644
index 000000000..e2d4f844d
--- /dev/null
+++ b/app/settings.gradle
@@ -0,0 +1 @@
+include ':app:dslv:library'
diff --git a/assets/testfile.mp3 b/app/src/androidTest/assets/testfile.mp3
index f15faadf3..f15faadf3 100644
--- a/assets/testfile.mp3
+++ 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/assets/LICENSE.html b/app/src/main/assets/LICENSE.html
index d38547791..d38547791 100644
--- a/assets/LICENSE.html
+++ b/app/src/main/assets/LICENSE.html
diff --git a/assets/LICENSE_APACHE_COMMONS.txt b/app/src/main/assets/LICENSE_APACHE_COMMONS.txt
index d64569567..d64569567 100644
--- a/assets/LICENSE_APACHE_COMMONS.txt
+++ b/app/src/main/assets/LICENSE_APACHE_COMMONS.txt
diff --git a/assets/LICENSE_BETTERPICKERS.txt b/app/src/main/assets/LICENSE_BETTERPICKERS.txt
index 80830ed73..80830ed73 100644
--- a/assets/LICENSE_BETTERPICKERS.txt
+++ b/app/src/main/assets/LICENSE_BETTERPICKERS.txt
diff --git a/assets/LICENSE_DSLV.txt b/app/src/main/assets/LICENSE_DSLV.txt
index 2a2de04a3..2a2de04a3 100644
--- a/assets/LICENSE_DSLV.txt
+++ b/app/src/main/assets/LICENSE_DSLV.txt
diff --git a/assets/LICENSE_FLATTR4J.txt b/app/src/main/assets/LICENSE_FLATTR4J.txt
index d64569567..d64569567 100644
--- a/assets/LICENSE_FLATTR4J.txt
+++ b/app/src/main/assets/LICENSE_FLATTR4J.txt
diff --git a/assets/LICENSE_JSOUP.txt b/app/src/main/assets/LICENSE_JSOUP.txt
index f3ef71dbf..f3ef71dbf 100644
--- a/assets/LICENSE_JSOUP.txt
+++ b/app/src/main/assets/LICENSE_JSOUP.txt
diff --git a/assets/LICENSE_NINE_OLD_ANDROIDS.txt b/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt
index d64569567..d64569567 100644
--- a/assets/LICENSE_NINE_OLD_ANDROIDS.txt
+++ b/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt
diff --git a/assets/LICENSE_OKHTTP.txt b/app/src/main/assets/LICENSE_OKHTTP.txt
index 90edcee40..90edcee40 100644
--- a/assets/LICENSE_OKHTTP.txt
+++ b/app/src/main/assets/LICENSE_OKHTTP.txt
diff --git a/assets/LICENSE_OKIO.txt b/app/src/main/assets/LICENSE_OKIO.txt
index d64569567..d64569567 100644
--- a/assets/LICENSE_OKIO.txt
+++ b/app/src/main/assets/LICENSE_OKIO.txt
diff --git a/assets/LICENSE_PICASSO.txt b/app/src/main/assets/LICENSE_PICASSO.txt
index 0bf6b9f8e..0bf6b9f8e 100644
--- a/assets/LICENSE_PICASSO.txt
+++ b/app/src/main/assets/LICENSE_PICASSO.txt
diff --git a/assets/LICENSE_PRESTO.txt b/app/src/main/assets/LICENSE_PRESTO.txt
index b4b1a8cf5..b4b1a8cf5 100644
--- a/assets/LICENSE_PRESTO.txt
+++ b/app/src/main/assets/LICENSE_PRESTO.txt
diff --git a/assets/Roboto-Light.ttf b/app/src/main/assets/Roboto-Light.ttf
index 13bf13af0..13bf13af0 100644
--- a/assets/Roboto-Light.ttf
+++ b/app/src/main/assets/Roboto-Light.ttf
Binary files differ
diff --git a/assets/Roboto.ttf b/app/src/main/assets/Roboto.ttf
index 0ba95c98c..0ba95c98c 100644
--- a/assets/Roboto.ttf
+++ b/app/src/main/assets/Roboto.ttf
Binary files differ
diff --git a/assets/about.html b/app/src/main/assets/about.html
index 115a0a5c9..115a0a5c9 100644
--- a/assets/about.html
+++ b/app/src/main/assets/about.html
diff --git a/assets/logo.png b/app/src/main/assets/logo.png
index d0e988a6d..d0e988a6d 100755
--- a/assets/logo.png
+++ 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/src/de/danoeh/antennapod/AppConfig.java b/app/src/main/java/de/danoeh/antennapod/AppConfig.java
index 24f13d4a3..24f13d4a3 100644
--- a/src/de/danoeh/antennapod/AppConfig.java
+++ b/app/src/main/java/de/danoeh/antennapod/AppConfig.java
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/src/de/danoeh/antennapod/activity/AboutActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java
index cf7de1709..cf7de1709 100644
--- a/src/de/danoeh/antennapod/activity/AboutActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java
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/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java
index bdb2d68ba..bdb2d68ba 100644
--- a/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java
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/src/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
index f5ae5a777..f5ae5a777 100644
--- a/src/de/danoeh/antennapod/fragment/AddFeedFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
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/src/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
index 5a71cb36b..5a71cb36b 100644
--- a/src/de/danoeh/antennapod/fragment/DownloadsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
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/src/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
index ec8f69368..ec8f69368 100644
--- a/src/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
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/src/de/danoeh/antennapod/spa/SPAUtil.java b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java
index 75cbd8b5a..75cbd8b5a 100644
--- a/src/de/danoeh/antennapod/spa/SPAUtil.java
+++ b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java
diff --git a/src/de/danoeh/antennapod/view/AspectRatioVideoView.java b/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java
index f930c912a..f930c912a 100644
--- a/src/de/danoeh/antennapod/view/AspectRatioVideoView.java
+++ b/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java
diff --git a/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml
index d3567dc31..d3567dc31 100644
--- a/res/anim/fade_in.xml
+++ b/app/src/main/res/anim/fade_in.xml
diff --git a/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml
index ddf12d13f..ddf12d13f 100644
--- a/res/anim/fade_out.xml
+++ b/app/src/main/res/anim/fade_out.xml
diff --git a/res/layout-land/audioplayer_activity.xml b/app/src/main/res/layout-land/audioplayer_activity.xml
index 8f8fdbee3..8f8fdbee3 100644
--- a/res/layout-land/audioplayer_activity.xml
+++ b/app/src/main/res/layout-land/audioplayer_activity.xml
diff --git a/res/layout-land/videoplayer_activity.xml b/app/src/main/res/layout-land/videoplayer_activity.xml
index f1e54f7c3..f1e54f7c3 100644
--- a/res/layout-land/videoplayer_activity.xml
+++ b/app/src/main/res/layout-land/videoplayer_activity.xml
diff --git a/res/layout-v14/authentication_dialog.xml b/app/src/main/res/layout-v14/authentication_dialog.xml
index ed05dab1c..ed05dab1c 100644
--- a/res/layout-v14/authentication_dialog.xml
+++ b/app/src/main/res/layout-v14/authentication_dialog.xml
diff --git a/res/layout-v14/directory_chooser.xml b/app/src/main/res/layout-v14/directory_chooser.xml
index f0bef72e4..f0bef72e4 100644
--- a/res/layout-v14/directory_chooser.xml
+++ b/app/src/main/res/layout-v14/directory_chooser.xml
diff --git a/res/layout-v14/download_authentication_activity.xml b/app/src/main/res/layout-v14/download_authentication_activity.xml
index c1fe55ceb..c1fe55ceb 100644
--- a/res/layout-v14/download_authentication_activity.xml
+++ b/app/src/main/res/layout-v14/download_authentication_activity.xml
diff --git a/res/layout-v14/opml_selection.xml b/app/src/main/res/layout-v14/opml_selection.xml
index 3133debd1..3133debd1 100644
--- a/res/layout-v14/opml_selection.xml
+++ b/app/src/main/res/layout-v14/opml_selection.xml
diff --git a/res/layout-v14/time_dialog.xml b/app/src/main/res/layout-v14/time_dialog.xml
index 7fd4309d5..7fd4309d5 100644
--- a/res/layout-v14/time_dialog.xml
+++ b/app/src/main/res/layout-v14/time_dialog.xml
diff --git a/res/layout/about.xml b/app/src/main/res/layout/about.xml
index acde9d786..acde9d786 100644
--- a/res/layout/about.xml
+++ b/app/src/main/res/layout/about.xml
diff --git a/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml
index 09502eb7b..09502eb7b 100644
--- a/res/layout/addfeed.xml
+++ b/app/src/main/res/layout/addfeed.xml
diff --git a/res/layout/audioplayer_activity.xml b/app/src/main/res/layout/audioplayer_activity.xml
index a879aad55..a879aad55 100644
--- a/res/layout/audioplayer_activity.xml
+++ b/app/src/main/res/layout/audioplayer_activity.xml
diff --git a/res/layout/authentication_dialog.xml b/app/src/main/res/layout/authentication_dialog.xml
index 82260eb43..82260eb43 100644
--- a/res/layout/authentication_dialog.xml
+++ b/app/src/main/res/layout/authentication_dialog.xml
diff --git a/res/layout/autoflattr_preference_dialog.xml b/app/src/main/res/layout/autoflattr_preference_dialog.xml
index fc2df30d7..fc2df30d7 100644
--- a/res/layout/autoflattr_preference_dialog.xml
+++ b/app/src/main/res/layout/autoflattr_preference_dialog.xml
diff --git a/res/layout/cover_fragment.xml b/app/src/main/res/layout/cover_fragment.xml
index f9c88ac02..f9c88ac02 100644
--- a/res/layout/cover_fragment.xml
+++ b/app/src/main/res/layout/cover_fragment.xml
diff --git a/res/layout/directory_chooser.xml b/app/src/main/res/layout/directory_chooser.xml
index 738c00842..738c00842 100644
--- a/res/layout/directory_chooser.xml
+++ b/app/src/main/res/layout/directory_chooser.xml
diff --git a/res/layout/download_authentication_activity.xml b/app/src/main/res/layout/download_authentication_activity.xml
index 69106c9b3..69106c9b3 100644
--- a/res/layout/download_authentication_activity.xml
+++ b/app/src/main/res/layout/download_authentication_activity.xml
diff --git a/res/layout/downloaded_episodeslist_item.xml b/app/src/main/res/layout/downloaded_episodeslist_item.xml
index 97003ce65..97003ce65 100644
--- a/res/layout/downloaded_episodeslist_item.xml
+++ b/app/src/main/res/layout/downloaded_episodeslist_item.xml
diff --git a/res/layout/downloadlist_item.xml b/app/src/main/res/layout/downloadlist_item.xml
index 49e0ea471..49e0ea471 100644
--- a/res/layout/downloadlist_item.xml
+++ b/app/src/main/res/layout/downloadlist_item.xml
diff --git a/res/layout/downloadlog_item.xml b/app/src/main/res/layout/downloadlog_item.xml
index 22d669097..22d669097 100644
--- a/res/layout/downloadlog_item.xml
+++ b/app/src/main/res/layout/downloadlog_item.xml
diff --git a/res/layout/ellipsize_start_listitem.xml b/app/src/main/res/layout/ellipsize_start_listitem.xml
index 161e1aa37..161e1aa37 100644
--- a/res/layout/ellipsize_start_listitem.xml
+++ b/app/src/main/res/layout/ellipsize_start_listitem.xml
diff --git a/res/layout/external_itemlist_item.xml b/app/src/main/res/layout/external_itemlist_item.xml
index 20c63c2cf..20c63c2cf 100644
--- a/res/layout/external_itemlist_item.xml
+++ b/app/src/main/res/layout/external_itemlist_item.xml
diff --git a/res/layout/external_player_fragment.xml b/app/src/main/res/layout/external_player_fragment.xml
index f084ccac1..f084ccac1 100644
--- a/res/layout/external_player_fragment.xml
+++ b/app/src/main/res/layout/external_player_fragment.xml
diff --git a/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml
index 6da200951..6da200951 100644
--- a/res/layout/feedinfo.xml
+++ b/app/src/main/res/layout/feedinfo.xml
diff --git a/res/layout/feeditem_dialog.xml b/app/src/main/res/layout/feeditem_dialog.xml
index e4a37d685..e4a37d685 100644
--- a/res/layout/feeditem_dialog.xml
+++ b/app/src/main/res/layout/feeditem_dialog.xml
diff --git a/res/layout/feeditemlist_header.xml b/app/src/main/res/layout/feeditemlist_header.xml
index 83c189bb5..83c189bb5 100644
--- a/res/layout/feeditemlist_header.xml
+++ b/app/src/main/res/layout/feeditemlist_header.xml
diff --git a/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml
index f3701de2c..f3701de2c 100644
--- a/res/layout/feeditemlist_item.xml
+++ b/app/src/main/res/layout/feeditemlist_item.xml
diff --git a/res/layout/flattr_auth.xml b/app/src/main/res/layout/flattr_auth.xml
index 9244b786d..9244b786d 100644
--- a/res/layout/flattr_auth.xml
+++ b/app/src/main/res/layout/flattr_auth.xml
diff --git a/res/layout/gpodnet_podcast_list.xml b/app/src/main/res/layout/gpodnet_podcast_list.xml
index 0112754ee..0112754ee 100644
--- a/res/layout/gpodnet_podcast_list.xml
+++ b/app/src/main/res/layout/gpodnet_podcast_list.xml
diff --git a/res/layout/gpodnet_podcast_listitem.xml b/app/src/main/res/layout/gpodnet_podcast_listitem.xml
index 1f6cdd1d0..1f6cdd1d0 100644
--- a/res/layout/gpodnet_podcast_listitem.xml
+++ b/app/src/main/res/layout/gpodnet_podcast_listitem.xml
diff --git a/res/layout/gpodnetauth_activity.xml b/app/src/main/res/layout/gpodnetauth_activity.xml
index c096c20cf..c096c20cf 100644
--- a/res/layout/gpodnetauth_activity.xml
+++ b/app/src/main/res/layout/gpodnetauth_activity.xml
diff --git a/res/layout/gpodnetauth_credentials.xml b/app/src/main/res/layout/gpodnetauth_credentials.xml
index 3e3c4e54f..3e3c4e54f 100644
--- a/res/layout/gpodnetauth_credentials.xml
+++ b/app/src/main/res/layout/gpodnetauth_credentials.xml
diff --git a/res/layout/gpodnetauth_device.xml b/app/src/main/res/layout/gpodnetauth_device.xml
index 33d3d2718..33d3d2718 100644
--- a/res/layout/gpodnetauth_device.xml
+++ b/app/src/main/res/layout/gpodnetauth_device.xml
diff --git a/res/layout/gpodnetauth_finish.xml b/app/src/main/res/layout/gpodnetauth_finish.xml
index 71873201a..71873201a 100644
--- a/res/layout/gpodnetauth_finish.xml
+++ b/app/src/main/res/layout/gpodnetauth_finish.xml
diff --git a/res/layout/itemdescription_listitem.xml b/app/src/main/res/layout/itemdescription_listitem.xml
index d6a3f6a16..d6a3f6a16 100644
--- a/res/layout/itemdescription_listitem.xml
+++ b/app/src/main/res/layout/itemdescription_listitem.xml
diff --git a/res/layout/listview_activity.xml b/app/src/main/res/layout/listview_activity.xml
index b276f506c..b276f506c 100644
--- a/res/layout/listview_activity.xml
+++ b/app/src/main/res/layout/listview_activity.xml
diff --git a/res/layout/main.xml b/app/src/main/res/layout/main.xml
index cfd59b87c..cfd59b87c 100644
--- a/res/layout/main.xml
+++ b/app/src/main/res/layout/main.xml
diff --git a/res/layout/nav_feedlistitem.xml b/app/src/main/res/layout/nav_feedlistitem.xml
index d94c9ada1..d94c9ada1 100644
--- a/res/layout/nav_feedlistitem.xml
+++ b/app/src/main/res/layout/nav_feedlistitem.xml
diff --git a/res/layout/nav_listitem.xml b/app/src/main/res/layout/nav_listitem.xml
index 9d70e7d7c..9d70e7d7c 100644
--- a/res/layout/nav_listitem.xml
+++ b/app/src/main/res/layout/nav_listitem.xml
diff --git a/res/layout/nav_section_item.xml b/app/src/main/res/layout/nav_section_item.xml
index 1f2fc7e3e..1f2fc7e3e 100644
--- a/res/layout/nav_section_item.xml
+++ b/app/src/main/res/layout/nav_section_item.xml
diff --git a/res/layout/new_episodes_fragment.xml b/app/src/main/res/layout/new_episodes_fragment.xml
index 63c712f57..63c712f57 100644
--- a/res/layout/new_episodes_fragment.xml
+++ b/app/src/main/res/layout/new_episodes_fragment.xml
diff --git a/res/layout/new_episodes_listitem.xml b/app/src/main/res/layout/new_episodes_listitem.xml
index dcef1f8fc..dcef1f8fc 100644
--- a/res/layout/new_episodes_listitem.xml
+++ b/app/src/main/res/layout/new_episodes_listitem.xml
diff --git a/res/layout/onlinefeedview_header.xml b/app/src/main/res/layout/onlinefeedview_header.xml
index 11ae1f644..11ae1f644 100644
--- a/res/layout/onlinefeedview_header.xml
+++ b/app/src/main/res/layout/onlinefeedview_header.xml
diff --git a/res/layout/opml_import.xml b/app/src/main/res/layout/opml_import.xml
index 919e30551..919e30551 100644
--- a/res/layout/opml_import.xml
+++ b/app/src/main/res/layout/opml_import.xml
diff --git a/res/layout/opml_selection.xml b/app/src/main/res/layout/opml_selection.xml
index d08ebd0bd..d08ebd0bd 100644
--- a/res/layout/opml_selection.xml
+++ b/app/src/main/res/layout/opml_selection.xml
diff --git a/res/layout/pager_fragment.xml b/app/src/main/res/layout/pager_fragment.xml
index cb7ae0151..cb7ae0151 100644
--- a/res/layout/pager_fragment.xml
+++ b/app/src/main/res/layout/pager_fragment.xml
diff --git a/res/layout/player_widget.xml b/app/src/main/res/layout/player_widget.xml
index b6946f7a8..b6946f7a8 100644
--- a/res/layout/player_widget.xml
+++ b/app/src/main/res/layout/player_widget.xml
diff --git a/res/layout/queue_fragment.xml b/app/src/main/res/layout/queue_fragment.xml
index 742411761..742411761 100644
--- a/res/layout/queue_fragment.xml
+++ b/app/src/main/res/layout/queue_fragment.xml
diff --git a/res/layout/queue_listitem.xml b/app/src/main/res/layout/queue_listitem.xml
index 4a55cd466..4a55cd466 100644
--- a/res/layout/queue_listitem.xml
+++ b/app/src/main/res/layout/queue_listitem.xml
diff --git a/res/layout/searchlist_item.xml b/app/src/main/res/layout/searchlist_item.xml
index b057a966d..b057a966d 100644
--- a/res/layout/searchlist_item.xml
+++ b/app/src/main/res/layout/searchlist_item.xml
diff --git a/res/layout/simplechapter_item.xml b/app/src/main/res/layout/simplechapter_item.xml
index 422458d5d..422458d5d 100644
--- a/res/layout/simplechapter_item.xml
+++ b/app/src/main/res/layout/simplechapter_item.xml
diff --git a/res/layout/storage_error.xml b/app/src/main/res/layout/storage_error.xml
index c1ee77262..c1ee77262 100644
--- a/res/layout/storage_error.xml
+++ b/app/src/main/res/layout/storage_error.xml
diff --git a/res/layout/time_dialog.xml b/app/src/main/res/layout/time_dialog.xml
index 95cc9a5a7..95cc9a5a7 100644
--- a/res/layout/time_dialog.xml
+++ b/app/src/main/res/layout/time_dialog.xml
diff --git a/res/menu/directory_chooser.xml b/app/src/main/res/menu/directory_chooser.xml
index 7735ffd2c..7735ffd2c 100644
--- a/res/menu/directory_chooser.xml
+++ b/app/src/main/res/menu/directory_chooser.xml
diff --git a/res/menu/feedinfo.xml b/app/src/main/res/menu/feedinfo.xml
index be50cb87d..be50cb87d 100644
--- a/res/menu/feedinfo.xml
+++ b/app/src/main/res/menu/feedinfo.xml
diff --git a/res/menu/feeditem.xml b/app/src/main/res/menu/feeditem.xml
index 5b25e8f2c..5b25e8f2c 100644
--- a/res/menu/feeditem.xml
+++ b/app/src/main/res/menu/feeditem.xml
diff --git a/res/menu/feeditem_dialog.xml b/app/src/main/res/menu/feeditem_dialog.xml
index f33b7502a..f33b7502a 100644
--- a/res/menu/feeditem_dialog.xml
+++ b/app/src/main/res/menu/feeditem_dialog.xml
diff --git a/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml
index f8eb7232e..f8eb7232e 100644
--- a/res/menu/feedlist.xml
+++ b/app/src/main/res/menu/feedlist.xml
diff --git a/res/menu/main.xml b/app/src/main/res/menu/main.xml
index c5b069b40..c5b069b40 100644
--- a/res/menu/main.xml
+++ b/app/src/main/res/menu/main.xml
diff --git a/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml
index 0eb2ab067..0eb2ab067 100644
--- a/res/menu/mediaplayer.xml
+++ b/app/src/main/res/menu/mediaplayer.xml
diff --git a/res/menu/new_episodes.xml b/app/src/main/res/menu/new_episodes.xml
index 4cf3b5fec..4cf3b5fec 100644
--- a/res/menu/new_episodes.xml
+++ b/app/src/main/res/menu/new_episodes.xml
diff --git a/res/menu/queue_context.xml b/app/src/main/res/menu/queue_context.xml
index 327600038..327600038 100644
--- a/res/menu/queue_context.xml
+++ b/app/src/main/res/menu/queue_context.xml
diff --git a/res/xml/player_widget_info.xml b/app/src/main/res/xml/player_widget_info.xml
index 831f6daf0..831f6daf0 100644
--- a/res/xml/player_widget_info.xml
+++ b/app/src/main/res/xml/player_widget_info.xml
diff --git a/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 5175acdcb..5175acdcb 100644
--- a/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
diff --git a/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml
index ee73aca8d..ee73aca8d 100644
--- a/res/xml/searchable.xml
+++ b/app/src/main/res/xml/searchable.xml
diff --git a/build.gradle b/build.gradle
index 673eff508..d59e200b9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,130 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
buildscript {
repositories {
- mavenCentral()
+ jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:0.13.2'
- }
-}
-apply plugin: 'com.android.application'
-
-repositories {
- mavenCentral()
-}
-dependencies {
- def libsdir = new File('libs');
- if (!libsdir.exists()) {
- println "Creating libs directory"
- libsdir.mkdir()
- }
- compile 'com.android.support:support-v4:20.0.0'
- compile 'com.android.support:appcompat-v7:20.0.0'
- compile 'org.apache.commons:commons-lang3:3.3.2'
- compile ('org.shredzone.flattr4j:flattr4j-core:2.11') {
- exclude group: 'org.apache.httpcomponents', module: 'httpcore'
- exclude group: 'org.apache.httpcomponents', module: 'httpclient'
- exclude group: 'org.json', module: 'json'
- }
- compile 'commons-io:commons-io:2.4'
- compile 'com.nineoldandroids:library:2.4.0'
- compile project(':submodules:dslv:library')
+ classpath 'com.android.tools.build:gradle:0.13.3'
- compile 'com.jayway.android.robotium:robotium-solo:5.2.1'
- compile ("com.doomonafireball.betterpickers:library:1.5.2") {
- exclude group: 'com.android.support', module: 'support-v4'
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
}
- compile 'org.jsoup:jsoup:1.8.1'
- compile 'com.squareup.picasso:picasso:2.3.4'
- compile 'com.squareup.okhttp:okhttp:2.0.0'
- compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0'
- compile 'com.squareup.okio:okio:1.0.1'
}
-android {
- compileSdkVersion 20
- buildToolsVersion "20.0"
-
- defaultConfig {
- minSdkVersion 10
- targetSdkVersion 20
- testApplicationId "de.test.antennapod"
- testInstrumentationRunner "instrumentationTest.de.test.antennapod.AntennaPodTestRunner"
- }
-
- signingConfigs {
- releaseConfig {
- if (project.hasProperty('releaseStoreFile')) {
- storeFile file(releaseStoreFile)
- } else {
- storeFile file('keystore')
- }
- if (project.hasProperty('releaseStorePassword')) {
- storePassword releaseStorePassword
- } else {
- storePassword "password"
- }
- if (project.hasProperty('releaseKeyAlias')) {
- keyAlias releaseKeyAlias
- } else {
- keyAlias "alias"
- }
- if (project.hasProperty('releaseKeyPassword')) {
- keyPassword releaseKeyPassword
- } else {
- keyPassword "password"
- }
- }
- }
-
- sourceSets {
- main {
- manifest.srcFile 'AndroidManifest.xml'
- java.srcDirs = ['src']
- resources.srcDirs = ['src']
- aidl.srcDirs = ['src']
- renderscript.srcDirs = ['src']
- res.srcDirs = ['res']
- assets.srcDirs = ['assets']
- jniLibs.srcDirs = ['jniLibs']
- }
- }
-
- buildTypes {
- def STRING = "String"
- def FLATTR_APP_KEY = "FLATTR_APP_KEY"
- def FLATTR_APP_SECRET = "FLATTR_APP_SECRET"
- def mFlattrAppKey = (project.hasProperty('flattrAppKey')) ? flattrAppKey : "\"\""
- def mFlattrAppSecret = (project.hasProperty('flattrAppSecret')) ? flattrAppSecret : "\"\""
-
- debug {
- applicationIdSuffix ".debug"
- buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey
- buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret
- }
- release {
- runProguard true
- proguardFile 'proguard.cfg'
- signingConfig signingConfigs.releaseConfig
- buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey
- buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret
- }
- }
-
- packagingOptions {
- exclude 'META-INF/LICENSE.txt'
- exclude 'META-INF/NOTICE.txt'
- }
-
- lintOptions {
- abortOnError false
- }
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_7
- targetCompatibility JavaVersion.VERSION_1_7
+allprojects {
+ repositories {
+ jcenter()
}
}
task wrapper(type: Wrapper) {
gradleVersion = '2.1'
-}
+} \ No newline at end of file
diff --git a/core/.gitignore b/core/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/core/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 000000000..132d68084
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,43 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 20
+ buildToolsVersion "20.0.0"
+
+ defaultConfig {
+ applicationId "de.danoeh.antennapod.core"
+ minSdkVersion 10
+ targetSdkVersion 20
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile 'com.android.support:appcompat-v7:20.0.0'
+ compile 'com.android.support:support-v4:20.0.0'
+ compile 'org.apache.commons:commons-lang3:3.3.2'
+ compile ('org.shredzone.flattr4j:flattr4j-core:2.10') {
+ exclude group: 'org.apache.httpcomponents', module: 'httpcore'
+ exclude group: 'org.apache.httpcomponents', module: 'httpclient'
+ exclude group: 'org.json', module: 'json'
+ }
+ compile 'commons-io:commons-io:2.4'
+ compile 'com.nineoldandroids:library:2.4.0'
+ compile 'com.jayway.android.robotium:robotium-solo:5.2.1'
+ compile ("com.doomonafireball.betterpickers:library:1.5.2") {
+ exclude group: 'com.android.support', module: 'support-v4'
+ }
+ compile 'org.jsoup:jsoup:1.7.3'
+ compile 'com.squareup.picasso:picasso:2.3.4'
+ compile 'com.squareup.okhttp:okhttp:2.0.0'
+ compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0'
+ compile 'com.squareup.okio:okio:1.0.0'
+}
diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro
new file mode 100644
index 000000000..41a9efda7
--- /dev/null
+++ b/core/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/daniel/bin/android-sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java
new file mode 100644
index 000000000..894bcfa63
--- /dev/null
+++ b/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java
@@ -0,0 +1,13 @@
+package de.danoeh.antennapod.core;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+} \ No newline at end of file
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c660cd805
--- /dev/null
+++ b/core/src/main/AndroidManifest.xml
@@ -0,0 +1,52 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="de.danoeh.antennapod.core">
+
+ <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" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name">
+
+ <service
+ android:name=".service.download.DownloadService"
+ android:enabled="true" />
+ <service
+ android:name=".service.playback.PlaybackService"
+ android:enabled="true"
+ android:exported="true"/>
+ <service
+ android:name=".service.GpodnetSyncService"
+ android:enabled="true"/>
+
+ <receiver
+ android:name=".receiver.MediaButtonReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name=".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/src/com/aocate/presto/service/IDeathCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl
index 6bdc76801..6bdc76801 100644
--- a/src/com/aocate/presto/service/IDeathCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl
index 7357e402e..7357e402e 100644
--- a/src/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl
index d5edea729..d5edea729 100644
--- a/src/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl
index 2c4f2df3e..2c4f2df3e 100644
--- a/src/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl
index 9dbd1d260..9dbd1d260 100644
--- a/src/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl
index 41223a97b..41223a97b 100644
--- a/src/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl
index 7be8f1237..7be8f1237 100644
--- a/src/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl
index 5bdda98b6..5bdda98b6 100644
--- a/src/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl
index a69c1cf34..a69c1cf34 100644
--- a/src/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl
diff --git a/src/com/aocate/presto/service/IPlayMedia_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl
index 12a6047de..12a6047de 100644
--- a/src/com/aocate/presto/service/IPlayMedia_0_8.aidl
+++ b/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl
diff --git a/src/com/aocate/media/AndroidMediaPlayer.java b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java
index 17ee74a13..17ee74a13 100644
--- a/src/com/aocate/media/AndroidMediaPlayer.java
+++ b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java
diff --git a/core/src/main/java/com/aocate/media/MediaPlayer.java b/core/src/main/java/com/aocate/media/MediaPlayer.java
new file mode 100644
index 000000000..c73c5219e
--- /dev/null
+++ b/core/src/main/java/com/aocate/media/MediaPlayer.java
@@ -0,0 +1,1278 @@
+// 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.
+
+package com.aocate.media;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.IBinder;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
+
+import de.danoeh.antennapod.core.BuildConfig;
+
+public class MediaPlayer {
+ public interface OnBufferingUpdateListener {
+ public abstract void onBufferingUpdate(MediaPlayer arg0, int percent);
+ }
+
+ public interface OnCompletionListener {
+ public abstract void onCompletion(MediaPlayer arg0);
+ }
+
+ public interface OnErrorListener {
+ public abstract boolean onError(MediaPlayer arg0, int what, int extra);
+ }
+
+ public interface OnInfoListener {
+ public abstract boolean onInfo(MediaPlayer arg0, int what, int extra);
+ }
+
+ public interface OnPitchAdjustmentAvailableChangedListener {
+ /**
+ * @param arg0 The owning media player
+ * @param pitchAdjustmentAvailable True if pitch adjustment is available, false if not
+ */
+ public abstract void onPitchAdjustmentAvailableChanged(
+ MediaPlayer arg0, boolean pitchAdjustmentAvailable);
+ }
+
+ public interface OnPreparedListener {
+ public abstract void onPrepared(MediaPlayer arg0);
+ }
+
+ public interface OnSeekCompleteListener {
+ public abstract void onSeekComplete(MediaPlayer arg0);
+ }
+
+ public interface OnSpeedAdjustmentAvailableChangedListener {
+ /**
+ * @param arg0 The owning media player
+ * @param speedAdjustmentAvailable True if speed adjustment is available, false if not
+ */
+ public abstract void onSpeedAdjustmentAvailableChanged(
+ MediaPlayer arg0, boolean speedAdjustmentAvailable);
+ }
+
+ public enum State {
+ IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED, PREPARING, PLAYBACK_COMPLETED, END, ERROR
+ }
+
+ private static Uri SPEED_ADJUSTMENT_MARKET_URI = Uri
+ .parse("market://details?id=com.aocate.presto");
+
+ private static Intent prestoMarketIntent = null;
+
+ public static final int MEDIA_ERROR_SERVER_DIED = android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED;
+ public static final int MEDIA_ERROR_UNKNOWN = android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN;
+ public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = android.media.MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK;
+
+ /**
+ * Indicates whether the specified action can be used as an intent. This
+ * method queries the package manager for installed packages that can
+ * respond to an intent with the specified action. If no suitable package is
+ * found, this method returns false.
+ *
+ * @param context The application's environment.
+ * @param action The Intent action to check for availability.
+ * @return True if an Intent with the specified action can be sent and
+ * responded to, false otherwise.
+ */
+ public static boolean isIntentAvailable(Context context, String action) {
+ final PackageManager packageManager = context.getPackageManager();
+ final Intent intent = new Intent(action);
+ List<ResolveInfo> list = packageManager.queryIntentServices(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return list.size() > 0;
+ }
+
+ /**
+ * Indicates whether the Presto library is installed
+ *
+ * @param context The context to use to query the package manager.
+ * @return True if the Presto library is installed, false if not.
+ */
+ public static boolean isPrestoLibraryInstalled(Context context) {
+ return isIntentAvailable(context, ServiceBackedMediaPlayer.INTENT_NAME);
+ }
+
+ /**
+ * Return an Intent that opens the Android Market page for the speed
+ * alteration library
+ *
+ * @return The Intent for the Presto library on the Android Market
+ */
+ public static Intent getPrestoMarketIntent() {
+ if (prestoMarketIntent == null) {
+ prestoMarketIntent = new Intent(Intent.ACTION_VIEW,
+ SPEED_ADJUSTMENT_MARKET_URI);
+ }
+ return prestoMarketIntent;
+ }
+
+ /**
+ * Open the Android Market page for the Presto library
+ *
+ * @param context The context from which to open the Android Market page
+ */
+ public static void openPrestoMarketIntent(Context context) {
+ context.startActivity(getPrestoMarketIntent());
+ }
+
+ private static final String MP_TAG = "AocateReplacementMediaPlayer";
+
+ private static final double PITCH_STEP_CONSTANT = 1.0594630943593;
+
+ private AndroidMediaPlayer amp = null;
+ // This is whether speed adjustment should be enabled (by the Service)
+ // To avoid the Service entirely, set useService to false
+ protected boolean enableSpeedAdjustment = true;
+ private int lastKnownPosition = 0;
+ // In some cases, we're going to have to replace the
+ // android.media.MediaPlayer on the fly, and we don't want to touch the
+ // wrong media player, so lock it way too much.
+ ReentrantLock lock = new ReentrantLock();
+ private int mAudioStreamType = AudioManager.STREAM_MUSIC;
+ private Context mContext;
+ private boolean mIsLooping = false;
+ private float mLeftVolume = 1f;
+ private float mPitchStepsAdjustment = 0f;
+ private float mRightVolume = 1f;
+ private float mSpeedMultiplier = 1f;
+ private int mWakeMode = 0;
+ MediaPlayerImpl mpi = null;
+ protected boolean pitchAdjustmentAvailable = false;
+ private ServiceBackedMediaPlayer sbmp = null;
+ protected boolean speedAdjustmentAvailable = false;
+
+ private Handler mServiceDisconnectedHandler = null;
+
+ // Some parts of state cannot be found by calling MediaPlayerImpl functions,
+ // so store our own state. This also helps copy state when changing
+ // implementations
+ State state = State.INITIALIZED;
+ String stringDataSource = null;
+ Uri uriDataSource = null;
+ private boolean useService = false;
+
+ // Naming Convention for Listeners
+ // Most listeners can both be set by clients and called by MediaPlayImpls
+ // There are a few that have to do things in this class as well as calling
+ // the function. In all cases, onX is what is called by MediaPlayerImpl
+ // If there is work to be done in this class, then the listener that is
+ // set by setX is X (with the first letter lowercase).
+ OnBufferingUpdateListener onBufferingUpdateListener = null;
+ OnCompletionListener onCompletionListener = null;
+ OnErrorListener onErrorListener = null;
+ OnInfoListener onInfoListener = null;
+
+ // Special case. Pitch adjustment ceases to be available when we switch
+ // to the android.media.MediaPlayer (though it is not guaranteed to be
+ // available when using the ServiceBackedMediaPlayer)
+ OnPitchAdjustmentAvailableChangedListener onPitchAdjustmentAvailableChangedListener = new OnPitchAdjustmentAvailableChangedListener() {
+ public void onPitchAdjustmentAvailableChanged(MediaPlayer arg0,
+ boolean pitchAdjustmentAvailable) {
+ lock.lock();
+ try {
+ Log
+ .d(
+ MP_TAG,
+ "onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged being called");
+ if (MediaPlayer.this.pitchAdjustmentAvailable != pitchAdjustmentAvailable) {
+ Log.d(MP_TAG, "Pitch adjustment state has changed from "
+ + MediaPlayer.this.pitchAdjustmentAvailable
+ + " to " + pitchAdjustmentAvailable);
+ MediaPlayer.this.pitchAdjustmentAvailable = pitchAdjustmentAvailable;
+ if (MediaPlayer.this.pitchAdjustmentAvailableChangedListener != null) {
+ MediaPlayer.this.pitchAdjustmentAvailableChangedListener
+ .onPitchAdjustmentAvailableChanged(arg0,
+ pitchAdjustmentAvailable);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+ };
+ OnPitchAdjustmentAvailableChangedListener pitchAdjustmentAvailableChangedListener = null;
+
+ MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer arg0) {
+ Log.d(MP_TAG, "onPreparedListener 242 setting state to PREPARED");
+ MediaPlayer.this.state = State.PREPARED;
+ if (MediaPlayer.this.preparedListener != null) {
+ Log.d(MP_TAG, "Calling preparedListener");
+ MediaPlayer.this.preparedListener.onPrepared(arg0);
+ }
+ Log.d(MP_TAG, "Wrap up onPreparedListener");
+ }
+ };
+
+ OnPreparedListener preparedListener = null;
+ OnSeekCompleteListener onSeekCompleteListener = null;
+
+ // Special case. Speed adjustment ceases to be available when we switch
+ // to the android.media.MediaPlayer (though it is not guaranteed to be
+ // available when using the ServiceBackedMediaPlayer)
+ OnSpeedAdjustmentAvailableChangedListener onSpeedAdjustmentAvailableChangedListener = new OnSpeedAdjustmentAvailableChangedListener() {
+ public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0,
+ boolean speedAdjustmentAvailable) {
+ lock.lock();
+ try {
+ Log
+ .d(
+ MP_TAG,
+ "onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged being called");
+ if (MediaPlayer.this.speedAdjustmentAvailable != speedAdjustmentAvailable) {
+ Log.d(MP_TAG, "Speed adjustment state has changed from "
+ + MediaPlayer.this.speedAdjustmentAvailable
+ + " to " + speedAdjustmentAvailable);
+ MediaPlayer.this.speedAdjustmentAvailable = speedAdjustmentAvailable;
+ if (MediaPlayer.this.speedAdjustmentAvailableChangedListener != null) {
+ MediaPlayer.this.speedAdjustmentAvailableChangedListener
+ .onSpeedAdjustmentAvailableChanged(arg0,
+ speedAdjustmentAvailable);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+ };
+ OnSpeedAdjustmentAvailableChangedListener speedAdjustmentAvailableChangedListener = null;
+
+ private int speedAdjustmentAlgorithm = SpeedAdjustmentAlgorithm.SONIC;
+
+ public MediaPlayer(final Context context) {
+ this(context, true);
+ }
+
+ public MediaPlayer(final Context context, boolean useService) {
+ this.mContext = context;
+ this.useService = useService;
+
+ // So here's the major problem
+ // Sometimes the service won't exist or won't be connected,
+ // so start with an android.media.MediaPlayer, and when
+ // the service is connected, use that from then on
+ this.mpi = this.amp = new AndroidMediaPlayer(this, context);
+
+ // setupMpi will go get the Service, if it can, then bring that
+ // implementation into sync
+ Log.d(MP_TAG, "setupMpi");
+ setupMpi(context);
+ }
+
+ private boolean invalidServiceConnectionConfiguration() {
+ if (!(this.mpi instanceof ServiceBackedMediaPlayer)) {
+ if (this.useService && isPrestoLibraryInstalled()) {
+ // In this case, the Presto library has been installed
+ // or something while playing sound
+ // We could be using the service, but we're not
+ Log.d(MP_TAG, "We could be using the service, but we're not 316");
+ return true;
+ }
+ // If useService is false, then we shouldn't be using the SBMP
+ // If the Presto library isn't installed, ditto
+ Log.d(MP_TAG, "this.mpi is not a ServiceBackedMediaPlayer, but we couldn't use it anyway 321");
+ return false;
+ } else {
+ if (BuildConfig.DEBUG && !(this.mpi instanceof ServiceBackedMediaPlayer))
+ throw new AssertionError();
+ if (this.useService && isPrestoLibraryInstalled()) {
+ // We should be using the service, and we are. Great!
+ Log.d(MP_TAG, "We could be using a ServiceBackedMediaPlayer and we are 327");
+ return false;
+ }
+ // We're trying to use the service when we shouldn't,
+ // that's an invalid configuration
+ Log.d(MP_TAG, "We're trying to use a ServiceBackedMediaPlayer but we shouldn't be 332");
+ return true;
+ }
+ }
+
+ private void setupMpi(final Context context) {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "setupMpi 336");
+ // Check if the client wants to use the service at all,
+ // then if we're already using the right kind of media player
+ if (this.useService && isPrestoLibraryInstalled()) {
+ if ((this.mpi != null)
+ && (this.mpi instanceof ServiceBackedMediaPlayer)) {
+ Log.d(MP_TAG, "Already using ServiceBackedMediaPlayer");
+ return;
+ }
+ if (this.sbmp == null) {
+ Log.d(MP_TAG, "Instantiating new ServiceBackedMediaPlayer 346");
+ this.sbmp = new ServiceBackedMediaPlayer(this, context,
+ new ServiceConnection() {
+ public void onServiceConnected(
+ ComponentName className,
+ final IBinder service) {
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ // This lock probably isn't granular
+ // enough
+ MediaPlayer.this.lock.lock();
+ Log.d(MP_TAG,
+ "onServiceConnected 257");
+ try {
+ MediaPlayer.this
+ .switchMediaPlayerImpl(
+ MediaPlayer.this.amp,
+ MediaPlayer.this.sbmp);
+ Log.d(MP_TAG, "End onServiceConnected 362");
+ } finally {
+ MediaPlayer.this.lock.unlock();
+ }
+ }
+ });
+ t.start();
+ }
+
+ public void onServiceDisconnected(
+ ComponentName className) {
+ MediaPlayer.this.lock.lock();
+ try {
+ // Can't get any more useful information
+ // out of sbmp
+ if (MediaPlayer.this.sbmp != null) {
+ MediaPlayer.this.sbmp.release();
+ }
+ // Unlike most other cases, sbmp gets set
+ // to null since there's nothing useful
+ // backing it now
+ MediaPlayer.this.sbmp = null;
+
+ if (mServiceDisconnectedHandler == null) {
+ mServiceDisconnectedHandler = new Handler(new Callback() {
+ public boolean handleMessage(Message msg) {
+ // switchMediaPlayerImpl won't try to
+ // clone anything from null
+ lock.lock();
+ try {
+ if (MediaPlayer.this.amp == null) {
+ // This should never be in this state
+ MediaPlayer.this.amp = new AndroidMediaPlayer(
+ MediaPlayer.this,
+ MediaPlayer.this.mContext);
+ }
+ // Use sbmp instead of null in case by some miracle it's
+ // been restored in the meantime
+ MediaPlayer.this.switchMediaPlayerImpl(
+ MediaPlayer.this.sbmp,
+ MediaPlayer.this.amp);
+ return true;
+ } finally {
+ lock.unlock();
+ }
+ }
+ });
+ }
+
+ // This code needs to execute on the
+ // original thread to instantiate
+ // the new object in the right place
+ mServiceDisconnectedHandler
+ .sendMessage(
+ mServiceDisconnectedHandler
+ .obtainMessage());
+ // Note that we do NOT want to set
+ // useService. useService is about
+ // what the user wants, not what they
+ // get
+ } finally {
+ MediaPlayer.this.lock.unlock();
+ }
+ }
+ }
+ );
+ }
+ switchMediaPlayerImpl(this.amp, this.sbmp);
+ } else {
+ if ((this.mpi != null)
+ && (this.mpi instanceof AndroidMediaPlayer)) {
+ Log.d(MP_TAG, "Already using AndroidMediaPlayer");
+ return;
+ }
+ if (this.amp == null) {
+ Log.d(MP_TAG, "Instantiating new AndroidMediaPlayer (this should be impossible)");
+ this.amp = new AndroidMediaPlayer(this, context);
+ }
+ switchMediaPlayerImpl(this.sbmp, this.amp);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void switchMediaPlayerImpl(MediaPlayerImpl from, MediaPlayerImpl to) {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "switchMediaPlayerImpl");
+ if ((from == to)
+ // Same object, nothing to synchronize
+ || (to == null)
+ // Nothing to copy to (maybe this should throw an error?)
+ || ((to instanceof ServiceBackedMediaPlayer) && !((ServiceBackedMediaPlayer) to).isConnected())
+ // ServiceBackedMediaPlayer hasn't yet connected, onServiceConnected will take care of the transition
+ || (MediaPlayer.this.state == State.END)) {
+ // State.END is after a release(), no further functions should
+ // be called on this class and from is likely to have problems
+ // retrieving state that won't be used anyway
+ return;
+ }
+ // Extract all that we can from the existing implementation
+ // and copy it to the new implementation
+
+ Log.d(MP_TAG, "switchMediaPlayerImpl(), current state is "
+ + this.state.toString());
+
+ to.reset();
+
+ // Do this first so we don't have to prepare the same
+ // data file twice
+ to.setEnableSpeedAdjustment(MediaPlayer.this.enableSpeedAdjustment);
+
+ // This is a reasonable place to set all of these,
+ // none of them require prepare() or the like first
+ to.setAudioStreamType(this.mAudioStreamType);
+ to.setSpeedAdjustmentAlgorithm(this.speedAdjustmentAlgorithm);
+ to.setLooping(this.mIsLooping);
+ to.setPitchStepsAdjustment(this.mPitchStepsAdjustment);
+ Log.d(MP_TAG, "Setting playback speed to " + this.mSpeedMultiplier);
+ to.setPlaybackSpeed(this.mSpeedMultiplier);
+ to.setVolume(MediaPlayer.this.mLeftVolume,
+ MediaPlayer.this.mRightVolume);
+ to.setWakeMode(this.mContext, this.mWakeMode);
+
+ Log.d(MP_TAG, "asserting at least one data source is null");
+ assert ((MediaPlayer.this.stringDataSource == null) || (MediaPlayer.this.uriDataSource == null));
+
+ if (uriDataSource != null) {
+ Log.d(MP_TAG, "switchMediaPlayerImpl(): uriDataSource != null");
+ try {
+ to.setDataSource(this.mContext, uriDataSource);
+ } catch (IllegalArgumentException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ if (stringDataSource != null) {
+ Log.d(MP_TAG,
+ "switchMediaPlayerImpl(): stringDataSource != null");
+ try {
+ to.setDataSource(stringDataSource);
+ } catch (IllegalArgumentException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ if ((this.state == State.PREPARED)
+ || (this.state == State.PREPARING)
+ || (this.state == State.PAUSED)
+ || (this.state == State.STOPPED)
+ || (this.state == State.STARTED)
+ || (this.state == State.PLAYBACK_COMPLETED)) {
+ Log.d(MP_TAG, "switchMediaPlayerImpl(): prepare and seek");
+ // Use prepare here instead of prepareAsync so that
+ // we wait for it to be ready before we try to use it
+ try {
+ to.muteNextOnPrepare();
+ to.prepare();
+ } catch (IllegalStateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ int seekPos = 0;
+ if (from != null) {
+ seekPos = from.getCurrentPosition();
+ } else if (this.lastKnownPosition < to.getDuration()) {
+ // This can happen if the Service unexpectedly
+ // disconnected. Because it would result in too much
+ // information being passed around, we don't constantly
+ // poll for the lastKnownPosition, but we'll save it
+ // when getCurrentPosition is called
+ seekPos = this.lastKnownPosition;
+ }
+ to.muteNextSeek();
+ to.seekTo(seekPos);
+ }
+ if ((from != null)
+ && from.isPlaying()) {
+ from.pause();
+ }
+ if ((this.state == State.STARTED)
+ || (this.state == State.PAUSED)
+ || (this.state == State.STOPPED)) {
+ Log.d(MP_TAG, "switchMediaPlayerImpl(): start");
+ if (to != null) {
+ to.start();
+ }
+ }
+
+ if (this.state == State.PAUSED) {
+ Log.d(MP_TAG, "switchMediaPlayerImpl(): paused");
+ if (to != null) {
+ to.pause();
+ }
+ } else if (this.state == State.STOPPED) {
+ Log.d(MP_TAG, "switchMediaPlayerImpl(): stopped");
+ if (to != null) {
+ to.stop();
+ }
+ }
+
+ this.mpi = to;
+
+ // Cheating here by relying on the side effect in
+ // on(Pitch|Speed)AdjustmentAvailableChanged
+ if ((to.canSetPitch() != this.pitchAdjustmentAvailable)
+ && (this.onPitchAdjustmentAvailableChangedListener != null)) {
+ this.onPitchAdjustmentAvailableChangedListener
+ .onPitchAdjustmentAvailableChanged(this, to
+ .canSetPitch());
+ }
+ if ((to.canSetSpeed() != this.speedAdjustmentAvailable)
+ && (this.onSpeedAdjustmentAvailableChangedListener != null)) {
+ this.onSpeedAdjustmentAvailableChangedListener
+ .onSpeedAdjustmentAvailableChanged(this, to
+ .canSetSpeed());
+ }
+ Log.d(MP_TAG, "switchMediaPlayerImpl() 625 " + this.state.toString());
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns true if pitch can be changed at this moment
+ *
+ * @return True if pitch can be changed
+ */
+ public boolean canSetPitch() {
+ lock.lock();
+ try {
+ return this.mpi.canSetPitch();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns true if speed can be changed at this moment
+ *
+ * @return True if speed can be changed
+ */
+ public boolean canSetSpeed() {
+ lock.lock();
+ try {
+ return this.mpi.canSetSpeed();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "finalize() 626");
+ this.release();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns the number of steps (in a musical scale) by which playback is
+ * currently shifted. When greater than zero, pitch is shifted up. When less
+ * than zero, pitch is shifted down.
+ *
+ * @return The number of steps pitch is currently shifted by
+ */
+ public float getCurrentPitchStepsAdjustment() {
+ lock.lock();
+ try {
+ return this.mpi.getCurrentPitchStepsAdjustment();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.getCurrentPosition()
+ * Accurate only to frame size of encoded data (26 ms for MP3s)
+ *
+ * @return Current position (in milliseconds)
+ */
+ public int getCurrentPosition() {
+ lock.lock();
+ try {
+ return (this.lastKnownPosition = this.mpi.getCurrentPosition());
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns the current speed multiplier. Defaults to 1.0 (normal speed)
+ *
+ * @return The current speed multiplier
+ */
+ public float getCurrentSpeedMultiplier() {
+ lock.lock();
+ try {
+ return this.mpi.getCurrentSpeedMultiplier();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.getDuration()
+ *
+ * @return Length of the track (in milliseconds)
+ */
+ public int getDuration() {
+ lock.lock();
+ try {
+ return this.mpi.getDuration();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Get the maximum value that can be passed to setPlaybackSpeed
+ *
+ * @return The maximum speed multiplier
+ */
+ public float getMaxSpeedMultiplier() {
+ lock.lock();
+ try {
+ return this.mpi.getMaxSpeedMultiplier();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Get the minimum value that can be passed to setPlaybackSpeed
+ *
+ * @return The minimum speed multiplier
+ */
+ public float getMinSpeedMultiplier() {
+ lock.lock();
+ try {
+ return this.mpi.getMinSpeedMultiplier();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Gets the version code of the backing service
+ *
+ * @return -1 if ServiceBackedMediaPlayer is not used, 0 if the service is not
+ * connected, otherwise the version code retrieved from the service
+ */
+ public int getServiceVersionCode() {
+ lock.lock();
+ try {
+ if (this.mpi instanceof ServiceBackedMediaPlayer) {
+ return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionCode();
+ } else {
+ return -1;
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Gets the version name of the backing service
+ *
+ * @return null if ServiceBackedMediaPlayer is not used, empty string if
+ * the service is not connected, otherwise the version name retrieved from
+ * the service
+ */
+ public String getServiceVersionName() {
+ lock.lock();
+ try {
+ if (this.mpi instanceof ServiceBackedMediaPlayer) {
+ return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionName();
+ } else {
+ return null;
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.isLooping()
+ *
+ * @return True if the track is looping
+ */
+ public boolean isLooping() {
+ lock.lock();
+ try {
+ return this.mpi.isLooping();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.isPlaying()
+ *
+ * @return True if the track is playing
+ */
+ public boolean isPlaying() {
+ lock.lock();
+ try {
+ return this.mpi.isPlaying();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns true if this MediaPlayer has access to the Presto
+ * library
+ *
+ * @return True if the Presto library is installed
+ */
+ public boolean isPrestoLibraryInstalled() {
+ if ((this.mpi == null) || (this.mpi.mContext == null)) {
+ return false;
+ }
+ return isPrestoLibraryInstalled(this.mpi.mContext);
+ }
+
+ /**
+ * Open the Android Market page in the same context as this MediaPlayer
+ */
+ public void openPrestoMarketIntent() {
+ if ((this.mpi != null) && (this.mpi.mContext != null)) {
+ openPrestoMarketIntent(this.mpi.mContext);
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.pause() Pauses the
+ * track
+ */
+ public void pause() {
+ lock.lock();
+ try {
+ if (invalidServiceConnectionConfiguration()) {
+ setupMpi(this.mpi.mContext);
+ }
+ this.state = State.PAUSED;
+ this.mpi.pause();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.prepare() Prepares the
+ * track. This or prepareAsync must be called before start()
+ */
+ public void prepare() throws IllegalStateException, IOException {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "prepare() 746 using " + ((this.mpi == null) ? "null (this shouldn't happen)" : this.mpi.getClass().toString()) + " state " + this.state.toString());
+ Log.d(MP_TAG, "onPreparedListener is: " + ((this.onPreparedListener == null) ? "null" : "non-null"));
+ Log.d(MP_TAG, "preparedListener is: " + ((this.preparedListener == null) ? "null" : "non-null"));
+ if (invalidServiceConnectionConfiguration()) {
+ setupMpi(this.mpi.mContext);
+ }
+ this.mpi.prepare();
+ this.state = State.PREPARED;
+ Log.d(MP_TAG, "prepare() finished 778");
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.prepareAsync()
+ * Prepares the track. This or prepare must be called before start()
+ */
+ public void prepareAsync() {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "prepareAsync() 779");
+ if (invalidServiceConnectionConfiguration()) {
+ setupMpi(this.mpi.mContext);
+ }
+ this.state = State.PREPARING;
+ this.mpi.prepareAsync();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.release() Releases the
+ * underlying resources used by the media player.
+ */
+ public void release() {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "Releasing MediaPlayer 791");
+
+ this.state = State.END;
+ if (this.amp != null) {
+ this.amp.release();
+ }
+ if (this.sbmp != null) {
+ this.sbmp.release();
+ }
+
+ this.onBufferingUpdateListener = null;
+ this.onCompletionListener = null;
+ this.onErrorListener = null;
+ this.onInfoListener = null;
+ this.preparedListener = null;
+ this.onPitchAdjustmentAvailableChangedListener = null;
+ this.pitchAdjustmentAvailableChangedListener = null;
+ Log.d(MP_TAG, "Setting onSeekCompleteListener to null 871");
+ this.onSeekCompleteListener = null;
+ this.onSpeedAdjustmentAvailableChangedListener = null;
+ this.speedAdjustmentAvailableChangedListener = null;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.reset() Resets the
+ * track to idle state
+ */
+ public void reset() {
+ lock.lock();
+ try {
+ this.state = State.IDLE;
+ this.stringDataSource = null;
+ this.uriDataSource = null;
+ this.mpi.reset();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.seekTo(int msec) Seeks
+ * to msec in the track
+ */
+ public void seekTo(int msec) throws IllegalStateException {
+ lock.lock();
+ try {
+ this.mpi.seekTo(msec);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setAudioStreamType(int
+ * streamtype) Sets the audio stream type.
+ */
+ public void setAudioStreamType(int streamtype) {
+ lock.lock();
+ try {
+ this.mAudioStreamType = streamtype;
+ this.mpi.setAudioStreamType(streamtype);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setDataSource(Context
+ * context, Uri uri) Sets uri as data source in the context given
+ */
+ public void setDataSource(Context context, Uri uri)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "In setDataSource(context, " + uri.toString() + "), using " + this.mpi.getClass().toString());
+ if (invalidServiceConnectionConfiguration()) {
+ setupMpi(this.mpi.mContext);
+ }
+ this.state = State.INITIALIZED;
+ this.stringDataSource = null;
+ this.uriDataSource = uri;
+ this.mpi.setDataSource(context, uri);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setDataSource(String
+ * path) Sets the data source of the track to a file given.
+ */
+ public void setDataSource(String path) throws IllegalArgumentException,
+ IllegalStateException, IOException {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "In setDataSource(context, " + path + ")");
+ if (invalidServiceConnectionConfiguration()) {
+ setupMpi(this.mpi.mContext);
+ }
+ this.state = State.INITIALIZED;
+ this.stringDataSource = path;
+ this.uriDataSource = null;
+ this.mpi.setDataSource(path);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Sets whether to use speed adjustment or not. Speed adjustment on is more
+ * computation-intensive than with it off.
+ *
+ * @param enableSpeedAdjustment Whether speed adjustment should be supported.
+ */
+ public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) {
+ lock.lock();
+ try {
+ this.enableSpeedAdjustment = enableSpeedAdjustment;
+ this.mpi.setEnableSpeedAdjustment(enableSpeedAdjustment);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setLooping(boolean
+ * loop) Sets the track to loop infinitely if loop is true, play once if
+ * loop is false
+ */
+ public void setLooping(boolean loop) {
+ lock.lock();
+ try {
+ this.mIsLooping = loop;
+ this.mpi.setLooping(loop);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Sets the number of steps (in a musical scale) by which playback is
+ * currently shifted. When greater than zero, pitch is shifted up. When less
+ * than zero, pitch is shifted down.
+ *
+ * @param pitchSteps The number of steps by which to shift playback
+ */
+ public void setPitchStepsAdjustment(float pitchSteps) {
+ lock.lock();
+ try {
+ this.mPitchStepsAdjustment = pitchSteps;
+ this.mpi.setPitchStepsAdjustment(pitchSteps);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Set the algorithm to use for changing the speed and pitch of audio
+ * See SpeedAdjustmentAlgorithm constants for more details
+ *
+ * @param algorithm The algorithm to use.
+ */
+ public void setSpeedAdjustmentAlgorithm(int algorithm) {
+ lock.lock();
+ try {
+ this.speedAdjustmentAlgorithm = algorithm;
+ if (this.mpi != null) {
+ this.mpi.setSpeedAdjustmentAlgorithm(algorithm);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private static float getPitchStepsAdjustment(float pitch) {
+ return (float) (Math.log(pitch) / (2 * Math.log(PITCH_STEP_CONSTANT)));
+ }
+
+ /**
+ * Sets the percentage by which pitch is currently shifted. When greater
+ * than zero, pitch is shifted up. When less than zero, pitch is shifted
+ * down
+ *
+ * @param f The percentage to shift pitch
+ */
+ public void setPlaybackPitch(float pitch) {
+ lock.lock();
+ try {
+ this.mPitchStepsAdjustment = getPitchStepsAdjustment(pitch);
+ this.mpi.setPlaybackPitch(pitch);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so on.
+ * Speed should never be set to 0 or below.
+ *
+ * @param f The speed multiplier to use for further playback
+ */
+ public void setPlaybackSpeed(float f) {
+ lock.lock();
+ try {
+ this.mSpeedMultiplier = f;
+ this.mpi.setPlaybackSpeed(f);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Sets whether to use speed adjustment or not. Speed adjustment on is more
+ * computation-intensive than with it off.
+ *
+ * @param enableSpeedAdjustment Whether speed adjustment should be supported.
+ */
+ public void setUseService(boolean useService) {
+ lock.lock();
+ try {
+ this.useService = useService;
+ setupMpi(this.mpi.mContext);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setVolume(float
+ * leftVolume, float rightVolume) Sets the stereo volume
+ */
+ public void setVolume(float leftVolume, float rightVolume) {
+ lock.lock();
+ try {
+ this.mLeftVolume = leftVolume;
+ this.mRightVolume = rightVolume;
+ this.mpi.setVolume(leftVolume, rightVolume);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setWakeMode(Context
+ * context, int mode) Acquires a wake lock in the context given. You must
+ * request the appropriate permissions in your AndroidManifest.xml file.
+ */
+ public void setWakeMode(Context context, int mode) {
+ lock.lock();
+ try {
+ this.mWakeMode = mode;
+ this.mpi.setWakeMode(context, mode);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to
+ * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener
+ * listener) Sets a listener to be used when a track completes playing.
+ */
+ public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) {
+ lock.lock();
+ try {
+ this.onBufferingUpdateListener = listener;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to
+ * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener
+ * listener) Sets a listener to be used when a track completes playing.
+ */
+ public void setOnCompletionListener(OnCompletionListener listener) {
+ lock.lock();
+ try {
+ this.onCompletionListener = listener;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to
+ * android.media.MediaPlayer.setOnErrorListener(OnErrorListener listener)
+ * Sets a listener to be used when a track encounters an error.
+ */
+ public void setOnErrorListener(OnErrorListener listener) {
+ lock.lock();
+ try {
+ this.onErrorListener = listener;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to
+ * android.media.MediaPlayer.setOnInfoListener(OnInfoListener listener) Sets
+ * a listener to be used when a track has info.
+ */
+ public void setOnInfoListener(OnInfoListener listener) {
+ lock.lock();
+ try {
+ this.onInfoListener = listener;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Sets a listener that will fire when pitch adjustment becomes available or
+ * stops being available
+ */
+ public void setOnPitchAdjustmentAvailableChangedListener(
+ OnPitchAdjustmentAvailableChangedListener listener) {
+ lock.lock();
+ try {
+ this.pitchAdjustmentAvailableChangedListener = listener;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to
+ * android.media.MediaPlayer.setOnPreparedListener(OnPreparedListener
+ * listener) Sets a listener to be used when a track finishes preparing.
+ */
+ public void setOnPreparedListener(OnPreparedListener listener) {
+ lock.lock();
+ Log.d(MP_TAG, " ++++++++++++++++++++++++++++++++++++++++++++ setOnPreparedListener");
+ try {
+ this.preparedListener = listener;
+ // For this one, we do not explicitly set the MediaPlayer or the
+ // Service listener. This is because in addition to calling the
+ // listener provided by the client, it's necessary to change
+ // state to PREPARED. See prepareAsync for implementation details
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to
+ * android.media.MediaPlayer.setOnSeekCompleteListener
+ * (OnSeekCompleteListener listener) Sets a listener to be used when a track
+ * finishes seeking.
+ */
+ public void setOnSeekCompleteListener(OnSeekCompleteListener listener) {
+ lock.lock();
+ try {
+ this.onSeekCompleteListener = listener;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Sets a listener that will fire when speed adjustment becomes available or
+ * stops being available
+ */
+ public void setOnSpeedAdjustmentAvailableChangedListener(
+ OnSpeedAdjustmentAvailableChangedListener listener) {
+ lock.lock();
+ try {
+ this.speedAdjustmentAvailableChangedListener = listener;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.start() Starts a track
+ * playing
+ */
+ public void start() {
+ lock.lock();
+ try {
+ Log.d(MP_TAG, "start() 1149");
+ if (invalidServiceConnectionConfiguration()) {
+ setupMpi(this.mpi.mContext);
+ }
+ this.state = State.STARTED;
+ Log.d(MP_TAG, "start() 1154");
+ this.mpi.start();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.stop() Stops a track
+ * playing and resets its position to the start.
+ */
+ public void stop() {
+ lock.lock();
+ try {
+ if (invalidServiceConnectionConfiguration()) {
+ setupMpi(this.mpi.mContext);
+ }
+ this.state = State.STOPPED;
+ this.mpi.stop();
+ } finally {
+ lock.unlock();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/aocate/media/MediaPlayerImpl.java b/core/src/main/java/com/aocate/media/MediaPlayerImpl.java
index 856ab47ce..856ab47ce 100644
--- a/src/com/aocate/media/MediaPlayerImpl.java
+++ b/core/src/main/java/com/aocate/media/MediaPlayerImpl.java
diff --git a/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java b/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java
new file mode 100644
index 000000000..702a23b0f
--- /dev/null
+++ b/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java
@@ -0,0 +1,1201 @@
+// 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.
+//
+// -----------------------------------------------------------------------
+// Compared to the original version, this class been slightly modified so
+// that any acquired WakeLocks are only held while the MediaPlayer is
+// playing (see the stayAwake method for more details).
+
+
+package com.aocate.media;
+
+import java.io.IOException;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import com.aocate.media.MediaPlayer.State;
+import com.aocate.presto.service.IDeathCallback_0_8;
+import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8;
+import com.aocate.presto.service.IOnCompletionListenerCallback_0_8;
+import com.aocate.presto.service.IOnErrorListenerCallback_0_8;
+import com.aocate.presto.service.IOnInfoListenerCallback_0_8;
+import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8;
+import com.aocate.presto.service.IOnPreparedListenerCallback_0_8;
+import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8;
+import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8;
+import com.aocate.presto.service.IPlayMedia_0_8;
+
+import de.danoeh.antennapod.core.BuildConfig;
+
+/**
+ * Class for connecting to remote speed-altering, media playing Service
+ * Note that there is unusually high coupling between MediaPlayer and this
+ * class. This is an unfortunate compromise, since the alternative was to
+ * track state in two different places in this code (plus the internal state
+ * of the remote media player).
+ * @author aocate
+ *
+ */
+public class ServiceBackedMediaPlayer extends MediaPlayerImpl {
+ static final String INTENT_NAME = "com.aocate.intent.PLAY_AUDIO_ADJUST_SPEED_0_8";
+
+ private static final String SBMP_TAG = "AocateServiceBackedMediaPlayer";
+
+ private ServiceConnection mPlayMediaServiceConnection = null;
+ protected IPlayMedia_0_8 pmInterface = null;
+ private Intent playMediaServiceIntent = null;
+ // In some cases, we're going to have to replace the
+ // android.media.MediaPlayer on the fly, and we don't want to touch the
+ // wrong media player.
+
+ private long sessionId = 0;
+ private boolean isErroring = false;
+ private int mAudioStreamType = AudioManager.STREAM_MUSIC;
+
+ private WakeLock mWakeLock = null;
+
+ // So here's the major problem
+ // Sometimes the service won't exist or won't be connected,
+ // so start with an android.media.MediaPlayer, and when
+ // the service is connected, use that from then on
+ public ServiceBackedMediaPlayer(MediaPlayer owningMediaPlayer, final Context context, final ServiceConnection serviceConnection) {
+ super(owningMediaPlayer, context);
+ Log.d(SBMP_TAG, "Instantiating ServiceBackedMediaPlayer 87");
+ this.playMediaServiceIntent =
+ new Intent(INTENT_NAME);
+ this.mPlayMediaServiceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ IPlayMedia_0_8 tmpPlayMediaInterface = IPlayMedia_0_8.Stub.asInterface((IBinder) service);
+
+ Log.d(SBMP_TAG, "Setting up pmInterface 94");
+ if (ServiceBackedMediaPlayer.this.sessionId == 0) {
+ try {
+ // The IDeathCallback isn't a conventional callback.
+ // It exists so that if the client ceases to exist,
+ // the Service becomes aware of that and can shut
+ // down whatever it needs to shut down
+ ServiceBackedMediaPlayer.this.sessionId = tmpPlayMediaInterface.startSession(new IDeathCallback_0_8.Stub() {
+ });
+ // This is really bad if this fails
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ Log.d(SBMP_TAG, "Assigning pmInterface");
+
+ ServiceBackedMediaPlayer.this.setOnBufferingUpdateCallback(tmpPlayMediaInterface);
+ ServiceBackedMediaPlayer.this.setOnCompletionCallback(tmpPlayMediaInterface);
+ ServiceBackedMediaPlayer.this.setOnErrorCallback(tmpPlayMediaInterface);
+ ServiceBackedMediaPlayer.this.setOnInfoCallback(tmpPlayMediaInterface);
+ ServiceBackedMediaPlayer.this.setOnPitchAdjustmentAvailableChangedListener(tmpPlayMediaInterface);
+ ServiceBackedMediaPlayer.this.setOnPreparedCallback(tmpPlayMediaInterface);
+ ServiceBackedMediaPlayer.this.setOnSeekCompleteCallback(tmpPlayMediaInterface);
+ ServiceBackedMediaPlayer.this.setOnSpeedAdjustmentAvailableChangedCallback(tmpPlayMediaInterface);
+
+ // In order to avoid race conditions from the sessionId or listener not being assigned
+ pmInterface = tmpPlayMediaInterface;
+
+ Log.d(SBMP_TAG, "Invoking onServiceConnected");
+ serviceConnection.onServiceConnected(name, service);
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ Log.d(SBMP_TAG, "onServiceDisconnected 114");
+
+ pmInterface = null;
+
+ sessionId = 0;
+
+ serviceConnection.onServiceDisconnected(name);
+ }
+ };
+
+ Log.d(SBMP_TAG, "Connecting PlayMediaService 124");
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private boolean ConnectPlayMediaService() {
+ Log.d(SBMP_TAG, "ConnectPlayMediaService()");
+
+ if (MediaPlayer.isIntentAvailable(mContext, INTENT_NAME)) {
+ Log.d(SBMP_TAG, INTENT_NAME + " is available");
+ if (pmInterface == null) {
+ try {
+ Log.d(SBMP_TAG, "Binding service");
+ return mContext.bindService(playMediaServiceIntent, mPlayMediaServiceConnection, Context.BIND_AUTO_CREATE);
+ } catch (Exception e) {
+ return false;
+ }
+ } else {
+ Log.d(SBMP_TAG, "Service already bound");
+ return true;
+ }
+ }
+ else {
+ Log.d(SBMP_TAG, INTENT_NAME + " is not available");
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if pitch can be changed at this moment
+ * @return True if pitch can be changed
+ */
+ @Override
+ public boolean canSetPitch() {
+ Log.d(SBMP_TAG, "canSetPitch() 155");
+
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set pitch if the service isn't connected
+ try {
+ return pmInterface.canSetPitch(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if speed can be changed at this moment
+ * @return True if speed can be changed
+ */
+ @Override
+ public boolean canSetSpeed() {
+ Log.d(SBMP_TAG, "canSetSpeed() 180");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the service isn't connected
+ try {
+ return pmInterface.canSetSpeed(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ return false;
+ }
+
+ void error(int what, int extra) {
+ owningMediaPlayer.lock.lock();
+ Log.e(SBMP_TAG, "error(" + what + ", " + extra + ")");
+ stayAwake(false);
+ try {
+ if (!this.isErroring) {
+ this.isErroring = true;
+ owningMediaPlayer.state = State.ERROR;
+ if (owningMediaPlayer.onErrorListener != null) {
+ if (owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra)) {
+ return;
+ }
+ }
+ if (owningMediaPlayer.onCompletionListener != null) {
+ owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer);
+ }
+ }
+ }
+ finally {
+ this.isErroring = false;
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ owningMediaPlayer.lock.lock();
+ try {
+ Log.d(SBMP_TAG, "finalize() 224");
+ this.release();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ /**
+ * Returns the number of steps (in a musical scale) by which playback is
+ * currently shifted. When greater than zero, pitch is shifted up.
+ * When less than zero, pitch is shifted down.
+ * @return The number of steps pitch is currently shifted by
+ */
+ @Override
+ public float getCurrentPitchStepsAdjustment() {
+ Log.d(SBMP_TAG, "getCurrentPitchStepsAdjustment() 240");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set pitch if the service isn't connected
+ try {
+ return pmInterface.getCurrentPitchStepsAdjustment(
+ ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ return 0f;
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.getCurrentPosition()
+ * @return Current position (in milliseconds)
+ */
+ @Override
+ public int getCurrentPosition() {
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ return pmInterface.getCurrentPosition(
+ ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the current speed multiplier. Defaults to 1.0 (normal speed)
+ * @return The current speed multiplier
+ */
+ @Override
+ public float getCurrentSpeedMultiplier() {
+ Log.d(SBMP_TAG, "getCurrentSpeedMultiplier() 286");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the service isn't connected
+ try {
+ return pmInterface.getCurrentSpeedMultiplier(
+ ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ return 1;
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.getDuration()
+ * @return Length of the track (in milliseconds)
+ */
+ @Override
+ public int getDuration() {
+ Log.d(SBMP_TAG, "getDuration() 311");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ return pmInterface.getDuration(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ return 0;
+ }
+
+ /**
+ * Get the maximum value that can be passed to setPlaybackSpeed
+ * @return The maximum speed multiplier
+ */
+ @Override
+ public float getMaxSpeedMultiplier() {
+ Log.d(SBMP_TAG, "getMaxSpeedMultiplier() 332");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the Service isn't connected
+ try {
+ return pmInterface.getMaxSpeedMultiplier(
+ ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ return 1f;
+ }
+
+ /**
+ * Get the minimum value that can be passed to setPlaybackSpeed
+ * @return The minimum speed multiplier
+ */
+ @Override
+ public float getMinSpeedMultiplier() {
+ Log.d(SBMP_TAG, "getMinSpeedMultiplier() 357");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the Service isn't connected
+ try {
+ return pmInterface.getMinSpeedMultiplier(
+ ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ return 1f;
+ }
+
+ public int getServiceVersionCode() {
+ Log.d(SBMP_TAG, "getVersionCode");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ return pmInterface.getVersionCode();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ return 0;
+ }
+
+ public String getServiceVersionName() {
+ Log.d(SBMP_TAG, "getVersionName");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ return pmInterface.getVersionName();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ return "";
+ }
+
+ public boolean isConnected() {
+ return (pmInterface != null);
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.isLooping()
+ * @return True if the track is looping
+ */
+ @Override
+ public boolean isLooping() {
+ Log.d(SBMP_TAG, "isLooping() 382");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ return pmInterface.isLooping(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ return false;
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.isPlaying()
+ * @return True if the track is playing
+ */
+ @Override
+ public boolean isPlaying() {
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ try {
+ return pmInterface.isPlaying(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.pause()
+ * Pauses the track
+ */
+ @Override
+ public void pause() {
+ Log.d(SBMP_TAG, "pause() 424");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.pause(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ stayAwake(false);
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.prepare()
+ * Prepares the track. This or prepareAsync must be called before start()
+ */
+ @Override
+ public void prepare() throws IllegalStateException, IOException {
+ Log.d(SBMP_TAG, "prepare() 444");
+ Log.d(SBMP_TAG, "onPreparedCallback is: " + ((this.mOnPreparedCallback == null) ? "null" : "non-null"));
+ if (pmInterface == null) {
+ Log.d(SBMP_TAG, "prepare: pmInterface is null");
+ if (!ConnectPlayMediaService()) {
+ Log.d(SBMP_TAG, "prepare: Failed to connect play media service");
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ Log.d(SBMP_TAG, "prepare: pmInterface isn't null");
+ try {
+ Log.d(SBMP_TAG, "prepare: Remote invoke pmInterface.prepare(" + ServiceBackedMediaPlayer.this.sessionId + ")");
+ pmInterface.prepare(ServiceBackedMediaPlayer.this.sessionId);
+ Log.d(SBMP_TAG, "prepare: prepared");
+ } catch (RemoteException e) {
+ Log.d(SBMP_TAG, "prepare: RemoteException");
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ Log.d(SBMP_TAG, "Done with prepare()");
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.prepareAsync()
+ * Prepares the track. This or prepare must be called before start()
+ */
+ @Override
+ public void prepareAsync() {
+ Log.d(SBMP_TAG, "prepareAsync() 469");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.prepareAsync(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.release()
+ * Releases the underlying resources used by the media player.
+ */
+ @Override
+ public void release() {
+ Log.d(SBMP_TAG, "release() 492");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ Log.d(SBMP_TAG, "release() 500");
+ try {
+ pmInterface.release(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ mContext.unbindService(this.mPlayMediaServiceConnection);
+ // Don't try to keep awake (if we were)
+ this.setWakeMode(mContext, 0);
+ pmInterface = null;
+ this.sessionId = 0;
+ }
+
+ if ((this.mWakeLock != null) && this.mWakeLock.isHeld()) {
+ Log.d(SBMP_TAG, "Releasing wakelock");
+ this.mWakeLock.release();
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.reset()
+ * Resets the track to idle state
+ */
+ @Override
+ public void reset() {
+ Log.d(SBMP_TAG, "reset() 523");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.reset(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ stayAwake(false);
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.seekTo(int msec)
+ * Seeks to msec in the track
+ */
+ @Override
+ public void seekTo(int msec) throws IllegalStateException {
+ Log.d(SBMP_TAG, "seekTo(" + msec + ")");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.seekTo(ServiceBackedMediaPlayer.this.sessionId, msec);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setAudioStreamType(int streamtype)
+ * Sets the audio stream type.
+ */
+ @Override
+ public void setAudioStreamType(int streamtype) {
+ Log.d(SBMP_TAG, "setAudioStreamType(" + streamtype + ")");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.setAudioStreamType(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mAudioStreamType);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setDataSource(Context context, Uri uri)
+ * Sets uri as data source in the context given
+ */
+ @Override
+ public void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException {
+ Log.d(SBMP_TAG, "setDataSource(context, uri)");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.setDataSourceUri(
+ ServiceBackedMediaPlayer.this.sessionId,
+ uri);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setDataSource(String path)
+ * Sets the data source of the track to a file given.
+ */
+ @Override
+ public void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException {
+ Log.d(SBMP_TAG, "setDataSource(path)");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface == null) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ else {
+ try {
+ pmInterface.setDataSourceString(
+ ServiceBackedMediaPlayer.this.sessionId,
+ path);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ }
+
+ /**
+ * Sets whether to use speed adjustment or not. Speed adjustment on is
+ * more computation-intensive than with it off.
+ * @param enableSpeedAdjustment Whether speed adjustment should be supported.
+ */
+ @Override
+ public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) {
+ // TODO: This has no business being here, I think
+ owningMediaPlayer.lock.lock();
+ Log.d(SBMP_TAG, "setEnableSpeedAdjustment(enableSpeedAdjustment)");
+ try {
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the Service isn't connected
+ try {
+ pmInterface.setEnableSpeedAdjustment(
+ ServiceBackedMediaPlayer.this.sessionId,
+ enableSpeedAdjustment);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setLooping(boolean loop)
+ * Sets the track to loop infinitely if loop is true, play once if loop is false
+ */
+ @Override
+ public void setLooping(boolean loop) {
+ Log.d(SBMP_TAG, "setLooping(" + loop + ")");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.setLooping(ServiceBackedMediaPlayer.this.sessionId, loop);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ /**
+ * Sets the number of steps (in a musical scale) by which playback is
+ * currently shifted. When greater than zero, pitch is shifted up.
+ * When less than zero, pitch is shifted down.
+ *
+ * @param pitchSteps The number of steps by which to shift playback
+ */
+ @Override
+ public void setPitchStepsAdjustment(float pitchSteps) {
+ Log.d(SBMP_TAG, "setPitchStepsAdjustment(" + pitchSteps + ")");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the Service isn't connected
+ try {
+ pmInterface.setPitchStepsAdjustment(
+ ServiceBackedMediaPlayer.this.sessionId,
+ pitchSteps);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ }
+
+ /**
+ * Sets the percentage by which pitch is currently shifted. When
+ * greater than zero, pitch is shifted up. When less than zero, pitch
+ * is shifted down
+ * @param f The percentage to shift pitch
+ */
+ @Override
+ public void setPlaybackPitch(float f) {
+ Log.d(SBMP_TAG, "setPlaybackPitch(" + f + ")");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the Service isn't connected
+ try {
+ pmInterface.setPlaybackPitch(
+ ServiceBackedMediaPlayer.this.sessionId,
+ f);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ }
+
+ /**
+ * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so
+ * on. Speed should never be set to 0 or below.
+ * @param f The speed multiplier to use for further playback
+ */
+ @Override
+ public void setPlaybackSpeed(float f) {
+ Log.d(SBMP_TAG, "setPlaybackSpeed(" + f + ")");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ if (pmInterface != null) {
+ // Can't set speed if the Service isn't connected
+ try {
+ pmInterface.setPlaybackSpeed(
+ ServiceBackedMediaPlayer.this.sessionId,
+ f);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ }
+
+ @Override
+ public void setSpeedAdjustmentAlgorithm(int algorithm) {
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.setSpeedAdjustmentAlgorithm(
+ ServiceBackedMediaPlayer.this.sessionId,
+ algorithm);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setVolume(float leftVolume, float rightVolume)
+ * Sets the stereo volume
+ */
+ @Override
+ public void setVolume(float leftVolume, float rightVolume) {
+ Log.d(SBMP_TAG, "setVolume(" + leftVolume + ", " + rightVolume + ")");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.setVolume(
+ ServiceBackedMediaPlayer.this.sessionId,
+ leftVolume,
+ rightVolume);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.setWakeMode(Context context, int mode)
+ * Acquires a wake lock in the context given. You must request the appropriate permissions
+ * in your AndroidManifest.xml file.
+ */
+ @Override
+ // This does not just call .setWakeMode() in the Service because doing so
+ // would add a permission requirement to the Service. Do it here, and it's
+ // the client app's responsibility to request that permission
+ public void setWakeMode(Context context, int mode) {
+ Log.d(SBMP_TAG, "setWakeMode(context, " + mode + ")");
+ if ((this.mWakeLock != null)
+ && (this.mWakeLock.isHeld())) {
+ this.mWakeLock.release();
+ }
+ if (mode != 0) {
+ if (this.mWakeLock == null) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ // Since mode can't be changed on the fly, we have to allocate a new one
+ this.mWakeLock = pm.newWakeLock(mode, this.getClass().getName());
+ this.mWakeLock.setReferenceCounted(false);
+ }
+
+ this.mWakeLock.acquire();
+ }
+ }
+
+ /**
+ * Changes the state of the WakeLock if it has been acquired.
+ * If no WakeLock has been acquired with setWakeMode, this method does nothing.
+ * */
+ private void stayAwake(boolean awake) {
+ if (BuildConfig.DEBUG) Log.d(SBMP_TAG, "stayAwake(" + awake + ")");
+ if (mWakeLock != null) {
+ if (awake && !mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ } else if (!awake && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+ }
+
+ private IOnBufferingUpdateListenerCallback_0_8.Stub mOnBufferingUpdateCallback = null;
+ private void setOnBufferingUpdateCallback(IPlayMedia_0_8 iface) {
+ try {
+ if (this.mOnBufferingUpdateCallback == null) {
+ mOnBufferingUpdateCallback = new IOnBufferingUpdateListenerCallback_0_8.Stub() {
+ public void onBufferingUpdate(int percent)
+ throws RemoteException {
+ owningMediaPlayer.lock.lock();
+ try {
+ if ((owningMediaPlayer.onBufferingUpdateListener != null)
+ && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) {
+ owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ };
+ }
+ iface.registerOnBufferingUpdateCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ mOnBufferingUpdateCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private IOnCompletionListenerCallback_0_8.Stub mOnCompletionCallback = null;
+ private void setOnCompletionCallback(IPlayMedia_0_8 iface) {
+ try {
+ if (this.mOnCompletionCallback == null) {
+ this.mOnCompletionCallback = new IOnCompletionListenerCallback_0_8.Stub() {
+ public void onCompletion() throws RemoteException {
+ owningMediaPlayer.lock.lock();
+ Log.d(SBMP_TAG, "onCompletionListener being called");
+ stayAwake(false);
+ try {
+ if (owningMediaPlayer.onCompletionListener != null) {
+ owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ };
+ }
+ iface.registerOnCompletionCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mOnCompletionCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private IOnErrorListenerCallback_0_8.Stub mOnErrorCallback = null;
+ private void setOnErrorCallback(IPlayMedia_0_8 iface) {
+ try {
+ if (this.mOnErrorCallback == null) {
+ this.mOnErrorCallback = new IOnErrorListenerCallback_0_8.Stub() {
+ public boolean onError(int what, int extra) throws RemoteException {
+ owningMediaPlayer.lock.lock();
+ stayAwake(false);
+ try {
+ if (owningMediaPlayer.onErrorListener != null) {
+ return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra);
+ }
+ return false;
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ };
+ }
+ iface.registerOnErrorCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mOnErrorCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private IOnInfoListenerCallback_0_8.Stub mOnInfoCallback = null;
+ private void setOnInfoCallback(IPlayMedia_0_8 iface) {
+ try {
+ if (this.mOnInfoCallback == null) {
+ this.mOnInfoCallback = new IOnInfoListenerCallback_0_8.Stub() {
+ public boolean onInfo(int what, int extra) throws RemoteException {
+ owningMediaPlayer.lock.lock();
+ try {
+ if ((owningMediaPlayer.onInfoListener != null)
+ && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) {
+ return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ return false;
+ }
+ };
+ }
+ iface.registerOnInfoCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mOnInfoCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnPitchAdjustmentAvailableChangedCallback = null;
+ private void setOnPitchAdjustmentAvailableChangedListener(IPlayMedia_0_8 iface) {
+ try {
+ if (this.mOnPitchAdjustmentAvailableChangedCallback == null) {
+ this.mOnPitchAdjustmentAvailableChangedCallback = new IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub() {
+ public void onPitchAdjustmentAvailableChanged(
+ boolean pitchAdjustmentAvailable)
+ throws RemoteException {
+ owningMediaPlayer.lock.lock();
+ try {
+ if (owningMediaPlayer.onPitchAdjustmentAvailableChangedListener != null) {
+ owningMediaPlayer.onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged(owningMediaPlayer, pitchAdjustmentAvailable);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ };
+ }
+ iface.registerOnPitchAdjustmentAvailableChangedCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mOnPitchAdjustmentAvailableChangedCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private IOnPreparedListenerCallback_0_8.Stub mOnPreparedCallback = null;
+ private void setOnPreparedCallback(IPlayMedia_0_8 iface) {
+ try {
+ if (this.mOnPreparedCallback == null) {
+ this.mOnPreparedCallback = new IOnPreparedListenerCallback_0_8.Stub() {
+ public void onPrepared() throws RemoteException {
+ owningMediaPlayer.lock.lock();
+ Log.d(SBMP_TAG, "setOnPreparedCallback.mOnPreparedCallback.onPrepared 1050");
+ try {
+ Log.d(SBMP_TAG, "owningMediaPlayer.onPreparedListener is " + ((owningMediaPlayer.onPreparedListener == null) ? "null" : "non-null"));
+ Log.d(SBMP_TAG, "owningMediaPlayer.mpi is " + ((owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this) ? "this" : "not this"));
+ ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.lock();
+ try {
+ if (ServiceBackedMediaPlayer.this.muteOnPreparedCount > 0) {
+ ServiceBackedMediaPlayer.this.muteOnPreparedCount--;
+ }
+ else {
+ ServiceBackedMediaPlayer.this.muteOnPreparedCount = 0;
+ if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) {
+ owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer);
+ }
+ }
+ }
+ finally {
+ ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.unlock();
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ };
+ }
+ iface.registerOnPreparedCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mOnPreparedCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private IOnSeekCompleteListenerCallback_0_8.Stub mOnSeekCompleteCallback = null;
+ private void setOnSeekCompleteCallback(IPlayMedia_0_8 iface) {
+ try {
+ if (this.mOnSeekCompleteCallback == null) {
+ this.mOnSeekCompleteCallback = new IOnSeekCompleteListenerCallback_0_8.Stub() {
+ public void onSeekComplete() throws RemoteException {
+ Log.d(SBMP_TAG, "onSeekComplete() 941");
+ owningMediaPlayer.lock.lock();
+ try {
+ if (ServiceBackedMediaPlayer.this.muteOnSeekCount > 0) {
+ Log.d(SBMP_TAG, "The next " + ServiceBackedMediaPlayer.this.muteOnSeekCount + " seek events are muted (counting this one)");
+ ServiceBackedMediaPlayer.this.muteOnSeekCount--;
+ }
+ else {
+ ServiceBackedMediaPlayer.this.muteOnSeekCount = 0;
+ Log.d(SBMP_TAG, "Attempting to invoke next seek event");
+ if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) {
+ Log.d(SBMP_TAG, "Invoking onSeekComplete");
+ owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer);
+ }
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ };
+ }
+ iface.registerOnSeekCompleteCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mOnSeekCompleteCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ private IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnSpeedAdjustmentAvailableChangedCallback = null;
+ private void setOnSpeedAdjustmentAvailableChangedCallback(IPlayMedia_0_8 iface) {
+ try {
+ Log.d(SBMP_TAG, "Setting the service of on speed adjustment available changed");
+ if (this.mOnSpeedAdjustmentAvailableChangedCallback == null) {
+ this.mOnSpeedAdjustmentAvailableChangedCallback = new IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub() {
+ public void onSpeedAdjustmentAvailableChanged(
+ boolean speedAdjustmentAvailable)
+ throws RemoteException {
+ owningMediaPlayer.lock.lock();
+ try {
+ if (owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener != null) {
+ owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged(owningMediaPlayer, speedAdjustmentAvailable);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ };
+ }
+ iface.registerOnSpeedAdjustmentAvailableChangedCallback(
+ ServiceBackedMediaPlayer.this.sessionId,
+ this.mOnSpeedAdjustmentAvailableChangedCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.start()
+ * Starts a track playing
+ */
+ @Override
+ public void start() {
+ Log.d(SBMP_TAG, "start()");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.start(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ stayAwake(true);
+ }
+
+ /**
+ * Functions identically to android.media.MediaPlayer.stop()
+ * Stops a track playing and resets its position to the start.
+ */
+ @Override
+ public void stop() {
+ Log.d(SBMP_TAG, "stop()");
+ if (pmInterface == null) {
+ if (!ConnectPlayMediaService()) {
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ }
+ try {
+ pmInterface.stop(ServiceBackedMediaPlayer.this.sessionId);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ }
+ stayAwake(false);
+ }
+} \ No newline at end of file
diff --git a/src/com/aocate/media/SpeedAdjustmentAlgorithm.java b/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java
index d337a0452..d337a0452 100644
--- a/src/com/aocate/media/SpeedAdjustmentAlgorithm.java
+++ b/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java
new file mode 100644
index 000000000..3bc1ce4eb
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java
@@ -0,0 +1,24 @@
+package de.danoeh.antennapod.core;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Callbacks related to the application in general
+ */
+public interface ApplicationCallbacks {
+
+ /**
+ * Returns a non-null instance of the application class
+ */
+ public Application getApplicationInstance();
+
+ /**
+ * Returns a non-null intent that starts the storage error
+ * activity.
+ */
+ public Intent getStorageErrorActivity(Context context);
+
+ public void setUpdateInterval(long updateInterval);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
new file mode 100644
index 000000000..e5e609f5f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.core;
+
+/**
+ * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables.
+ * Apps using the core module of AntennaPod should register implementations of all interfaces here.
+ */
+public class ClientConfig {
+
+ /**
+ * Should be used when setting User-Agent header for HTTP-requests.
+ */
+ public static String USER_AGENT;
+
+ public static ApplicationCallbacks applicationCallbacks;
+
+ public static DownloadServiceCallbacks downloadServiceCallbacks;
+
+ public static PlaybackServiceCallbacks playbackServiceCallbacks;
+
+ public static GpodnetCallbacks gpodnetCallbacks;
+
+ public static FlattrCallbacks flattrCallbacks;
+
+ public static StorageCallbacks storageCallbacks;
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java
new file mode 100644
index 000000000..286e830c5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java
@@ -0,0 +1,58 @@
+package de.danoeh.antennapod.core;
+
+import android.app.PendingIntent;
+import android.content.Context;
+
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+
+/**
+ * Callbacks for the DownloadService of the core module
+ */
+public interface DownloadServiceCallbacks {
+
+ /**
+ * Returns a PendingIntent for a notification the main notification of the DownloadService.
+ * <p/>
+ * The PendingIntent takes the users to a screen where they can observe all currently running
+ * downloads.
+ *
+ * @return A non-null PendingIntent for the notification.
+ */
+ public PendingIntent getNotificationContentIntent(Context context);
+
+ /**
+ * Returns a PendingIntent for a notification that tells the user to enter a username
+ * or a password for a requested download.
+ * <p/>
+ * The PendingIntent takes users to an Activity that lets the user enter their username
+ * and password to retry the download.
+ *
+ * @return A non-null PendingIntent for the notification.
+ */
+ public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request);
+
+ /**
+ * Returns a PendingIntent for notification that notifies the user about the completion of downloads
+ * along with information about failed and successful downloads.
+ * <p/>
+ * The PendingIntent takes users to an activity where they can look at all successful and failed downloads.
+ *
+ * @return A non-null PendingIntent for the notification or null if shouldCreateReport()==false
+ */
+ public PendingIntent getReportNotificationContentIntent(Context context);
+
+ /**
+ * Called by the FeedSyncThread after a feed has been downloaded and parsed.
+ *
+ * @param feed The non-null feed that has been parsed.
+ */
+ public void onFeedParsed(Context context, Feed feed);
+
+ /**
+ * Returns true if the DownloadService should create a report that shows the number of failed
+ * downloads when the service shuts down.
+ * */
+ public boolean shouldCreateReport();
+}
+
diff --git a/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java
new file mode 100644
index 000000000..cee1029d8
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java
@@ -0,0 +1,36 @@
+package de.danoeh.antennapod.core;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import org.shredzone.flattr4j.oauth.AccessToken;
+
+/**
+ * Callbacks for the flattr integration of the app.
+ */
+public interface FlattrCallbacks {
+
+ /**
+ * Returns if true if the flattr integration should be activated,
+ * false otherwise.
+ */
+ public boolean flattrEnabled();
+
+ /**
+ * Returns an intent that starts the activity that is responsible for
+ * letting users log into their flattr account.
+ *
+ * @return The intent that starts the authentication activity or null
+ * if flattr integration is disabled (i.e. flattrEnabled() == false).
+ */
+ public Intent getFlattrAuthenticationActivityIntent(Context context);
+
+ public PendingIntent getFlattrFailedNotificationContentIntent(Context context);
+
+ public String getFlattrAppKey();
+
+ public String getFlattrAppSecret();
+
+ public void handleFlattrAuthenticationSuccess(AccessToken token);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java
new file mode 100644
index 000000000..6174bce29
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java
@@ -0,0 +1,27 @@
+package de.danoeh.antennapod.core;
+
+import android.app.PendingIntent;
+import android.content.Context;
+
+/**
+ * Callbacks related to the gpodder.net integration of the core module
+ */
+public interface GpodnetCallbacks {
+
+
+ /**
+ * Returns if true if the gpodder.net integration should be activated,
+ * false otherwise.
+ */
+ public boolean gpodnetEnabled();
+
+ /**
+ * Returns a PendingIntent for the error notification of the GpodnetSyncService.
+ * <p/>
+ * What the PendingIntent does may be implementation-specific.
+ *
+ * @return A PendingIntent for the notification or null if gpodder.net integration
+ * has been disabled (i.e. gpodnetEnabled() == false).
+ */
+ public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java
new file mode 100644
index 000000000..fb01fa703
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java
@@ -0,0 +1,33 @@
+package de.danoeh.antennapod.core;
+
+import android.content.Context;
+import android.content.Intent;
+
+import de.danoeh.antennapod.core.feed.MediaType;
+
+/**
+ * Callbacks for the PlaybackService of the core module
+ */
+public interface PlaybackServiceCallbacks {
+
+ /**
+ * Returns an intent which starts an audio- or videoplayer, depending on the
+ * type of media that is being played.
+ *
+ * @param mediaType The type of media that is being played.
+ * @return A non-null activity intent.
+ */
+ public Intent getPlayerActivityIntent(Context context, MediaType mediaType);
+
+ /**
+ * Returns true if the PlaybackService should load new episodes from the queue when playback ends
+ * and false if the PlaybackService should ignore the queue and load no more episodes when playback
+ * finishes.
+ */
+ public boolean useQueue();
+
+ /**
+ * Returns a drawable resource that is used for the notification of the playback service.
+ */
+ public int getNotificationIconResource(Context context);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java
new file mode 100644
index 000000000..5d1a0fffc
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java
@@ -0,0 +1,27 @@
+package de.danoeh.antennapod.core;
+
+import android.database.sqlite.SQLiteDatabase;
+
+/**
+ * Callbacks for the classes in the storage package of the core module.
+ */
+public interface StorageCallbacks {
+
+ /**
+ * Returns the current version of the database.
+ *
+ * @return The non-negative version number of the database.
+ */
+ public int getDatabaseVersion();
+
+ /**
+ * Upgrades the given database from an old version to a newer version.
+ *
+ * @param db The database that is supposed to be upgraded.
+ * @param oldVersion The old version of the database.
+ * @param newVersion The version that the database is supposed to be upgraded to.
+ */
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java
new file mode 100644
index 000000000..a13130082
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java
@@ -0,0 +1,177 @@
+package de.danoeh.antennapod.core.asynctask;
+
+import android.app.Activity;
+import android.content.*;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.core.service.download.Downloader;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Provides access to the DownloadService's list of items that are currently being downloaded.
+ * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause()
+ * should be called in the activity's onResume() and onPause() methods
+ */
+public class DownloadObserver {
+ private static final String TAG = "DownloadObserver";
+
+ /**
+ * Time period between update notifications.
+ */
+ public static final int WAITING_INTERVAL_MS = 3000;
+
+ private volatile Activity activity;
+ private final Handler handler;
+ private final Callback callback;
+
+ private DownloadService downloadService = null;
+ private AtomicBoolean mIsBound = new AtomicBoolean(false);
+
+ private Thread refresherThread;
+ private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false);
+
+
+ /**
+ * Creates a new download observer.
+ *
+ * @param activity Used for registering receivers
+ * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread.
+ * @param callback Callback methods for posting content updates
+ * @throws java.lang.IllegalArgumentException if one of the arguments is null.
+ */
+ public DownloadObserver(Activity activity, Handler handler, Callback callback) {
+ Validate.notNull(activity);
+ Validate.notNull(handler);
+ Validate.notNull(callback);
+
+ this.activity = activity;
+ this.handler = handler;
+ this.callback = callback;
+ }
+
+ public void onResume() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed");
+ activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED));
+ connectToDownloadService();
+ }
+
+ public void onPause() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver paused");
+ try {
+ activity.unregisterReceiver(contentChangedReceiver);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ }
+ try {
+ activity.unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ }
+ stopRefresher();
+ }
+
+ private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // reconnect to DownloadService if connection has been closed
+ if (downloadService == null) {
+ connectToDownloadService();
+ }
+ callback.onContentChanged();
+ startRefresher();
+ }
+ };
+
+ public interface Callback {
+ void onContentChanged();
+
+ void onDownloadDataAvailable(List<Downloader> downloaderList);
+ }
+
+ private void connectToDownloadService() {
+ activity.bindService(new Intent(activity, DownloadService.class), mConnection, 0);
+ }
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceDisconnected(ComponentName className) {
+ downloadService = null;
+ mIsBound.set(false);
+ stopRefresher();
+ Log.i(TAG, "Closed connection with DownloadService.");
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ downloadService = ((DownloadService.LocalBinder) service)
+ .getService();
+ mIsBound.set(true);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Connection to service established");
+ List<Downloader> downloaderList = downloadService.getDownloads();
+ if (downloaderList != null && !downloaderList.isEmpty()) {
+ callback.onDownloadDataAvailable(downloaderList);
+ startRefresher();
+ }
+ }
+ };
+
+ private void stopRefresher() {
+ if (refresherThread != null) {
+ refresherThread.interrupt();
+ }
+ }
+
+ private void startRefresher() {
+ if (refresherThread == null || refresherThread.isInterrupted()) {
+ refresherThread = new Thread(new RefresherThread());
+ refresherThread.start();
+ }
+ }
+
+ private class RefresherThread implements Runnable {
+
+ public void run() {
+ refresherThreadRunning.set(true);
+ while (!Thread.interrupted()) {
+ try {
+ Thread.sleep(WAITING_INTERVAL_MS);
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Refresher thread was interrupted");
+ }
+ if (mIsBound.get()) {
+ postUpdate();
+ }
+ }
+ refresherThreadRunning.set(false);
+ }
+
+ private void postUpdate() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onContentChanged();
+ if (downloadService != null) {
+ List<Downloader> downloaderList = downloadService.getDownloads();
+ if (downloaderList == null || downloaderList.isEmpty()) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public void setActivity(Activity activity) {
+ Validate.notNull(activity);
+ this.activity = activity;
+ }
+
+}
+
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java
new file mode 100644
index 000000000..255b95119
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java
@@ -0,0 +1,74 @@
+package de.danoeh.antennapod.core.asynctask;
+
+import android.annotation.SuppressLint;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.os.AsyncTask;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.storage.DBWriter;
+
+import java.util.concurrent.ExecutionException;
+
+/** Removes a feed in the background. */
+public class FeedRemover extends AsyncTask<Void, Void, Void> {
+ Context context;
+ ProgressDialog dialog;
+ Feed feed;
+
+ public FeedRemover(Context context, Feed feed) {
+ super();
+ this.context = context;
+ this.feed = feed;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ DBWriter.deleteFeed(context, feed.getId()).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ protected void onCancelled() {
+ dialog.dismiss();
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ dialog.dismiss();
+ }
+
+ @Override
+ protected void onPreExecute() {
+ dialog = new ProgressDialog(context);
+ dialog.setMessage(context.getString(R.string.feed_remover_msg));
+ dialog.setOnCancelListener(new OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ cancel(true);
+
+ }
+
+ });
+ dialog.show();
+ }
+
+ @SuppressLint("NewApi")
+ public void executeAsync() {
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ execute();
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java
new file mode 100644
index 000000000..5d2d5d441
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java
@@ -0,0 +1,237 @@
+package de.danoeh.antennapod.core.asynctask;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.widget.Toast;
+
+import org.apache.commons.lang3.Validate;
+import org.shredzone.flattr4j.exception.FlattrException;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.core.util.flattr.FlattrThing;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+
+/**
+ * Performs a click action in a background thread.
+ * <p/>
+ * When started, the flattr click worker will try to flattr every item that is in the flattr queue. If no network
+ * connection is available it will shut down immediately. The FlattrClickWorker can also be given one additional
+ * FlattrThing which will be flattrd immediately.
+ * <p/>
+ * The FlattrClickWorker will display a toast notification for every item that has been flattrd. If the FlattrClickWorker failed
+ * to flattr something, a notification will be displayed.
+ */
+public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorker.ExitCode> {
+ protected static final String TAG = "FlattrClickWorker";
+
+ private static final int NOTIFICATION_ID = 4;
+
+ private final Context context;
+
+ public static enum ExitCode {EXIT_NORMAL, NO_TOKEN, NO_NETWORK, NO_THINGS}
+
+ private volatile int countFailed = 0;
+ private volatile int countSuccess = 0;
+
+ private volatile FlattrThing extraFlattrThing;
+
+ /**
+ * Only relevant if just one thing is flattrd
+ */
+ private volatile FlattrException exception;
+
+ /**
+ * Creates a new FlattrClickWorker which will only flattr all things in the queue.
+ * <p/>
+ * The FlattrClickWorker has to be started by calling executeAsync().
+ *
+ * @param context A context for accessing the database and posting notifications. Must not be null.
+ */
+ public FlattrClickWorker(Context context) {
+ Validate.notNull(context);
+ this.context = context.getApplicationContext();
+ }
+
+ /**
+ * Creates a new FlattrClickWorker which will flattr all things in the queue and one additional
+ * FlattrThing.
+ * <p/>
+ * The FlattrClickWorker has to be started by calling executeAsync().
+ *
+ * @param context A context for accessing the database and posting notifications. Must not be null.
+ * @param extraFlattrThing The additional thing to flattr
+ */
+ public FlattrClickWorker(Context context, FlattrThing extraFlattrThing) {
+ this(context);
+ this.extraFlattrThing = extraFlattrThing;
+ }
+
+
+ @Override
+ protected ExitCode doInBackground(Void... params) {
+
+ if (!FlattrUtils.hasToken()) {
+ return ExitCode.NO_TOKEN;
+ }
+
+ if (!NetworkUtils.networkAvailable(context)) {
+ return ExitCode.NO_NETWORK;
+ }
+
+ final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue(context);
+ if (extraFlattrThing != null) {
+ flattrQueue.add(extraFlattrThing);
+ } else if (flattrQueue.size() == 1) {
+ // if only one item is flattrd, the report can specifically mentioned that this item has failed
+ extraFlattrThing = flattrQueue.get(0);
+ }
+
+ if (flattrQueue.isEmpty()) {
+ return ExitCode.NO_THINGS;
+ }
+
+ List<Future> dbFutures = new LinkedList<Future>();
+ for (FlattrThing thing : flattrQueue) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Processing " + thing.getTitle());
+
+ try {
+ thing.getFlattrStatus().setUnflattred(); // pop from queue to prevent unflattrable things from getting stuck in flattr queue infinitely
+ FlattrUtils.clickUrl(context, thing.getPaymentLink());
+ thing.getFlattrStatus().setFlattred();
+ publishProgress(R.string.flattr_click_success);
+ countSuccess++;
+
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ countFailed++;
+ if (countFailed == 1) {
+ exception = e;
+ }
+ }
+
+ Future<?> f = DBWriter.setFlattredStatus(context, thing, false);
+ if (f != null) {
+ dbFutures.add(f);
+ }
+ }
+
+ for (Future f : dbFutures) {
+ try {
+ f.get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return ExitCode.EXIT_NORMAL;
+ }
+
+ @Override
+ protected void onPostExecute(ExitCode exitCode) {
+ super.onPostExecute(exitCode);
+ switch (exitCode) {
+ case EXIT_NORMAL:
+ if (countFailed > 0) {
+ postFlattrFailedNotification();
+ }
+ break;
+ case NO_NETWORK:
+ postToastNotification(R.string.flattr_click_enqueued);
+ break;
+ case NO_TOKEN:
+ postNoTokenNotification();
+ break;
+ case NO_THINGS: // nothing to notify here
+ break;
+ }
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... values) {
+ super.onProgressUpdate(values);
+ postToastNotification(values[0]);
+ }
+
+ private void postToastNotification(int msg) {
+ Toast.makeText(context, context.getString(msg), Toast.LENGTH_LONG).show();
+ }
+
+ private void postNoTokenNotification() {
+ PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
+ ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context), 0);
+
+ Notification notification = new NotificationCompat.Builder(context)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg)))
+ .setContentIntent(contentIntent)
+ .setContentTitle(context.getString(R.string.no_flattr_token_title))
+ .setTicker(context.getString(R.string.no_flattr_token_title))
+ .setSmallIcon(R.drawable.stat_notify_sync_error)
+ .setOngoing(false)
+ .setAutoCancel(true)
+ .build();
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification);
+ }
+
+ private void postFlattrFailedNotification() {
+ if (countFailed == 0) {
+ return;
+ }
+
+ PendingIntent contentIntent = ClientConfig.flattrCallbacks.getFlattrFailedNotificationContentIntent(context);
+ String title;
+ String subtext;
+
+ if (countFailed == 1) {
+ title = context.getString(R.string.flattrd_failed_label);
+ String exceptionMsg = (exception.getMessage() != null) ? exception.getMessage() : "";
+ subtext = context.getString(R.string.flattr_click_failure, extraFlattrThing.getTitle())
+ + "\n" + exceptionMsg;
+ } else {
+ title = context.getString(R.string.flattrd_label);
+ subtext = context.getString(R.string.flattr_click_success_count, countSuccess) + "\n"
+ + context.getString(R.string.flattr_click_failure_count, countFailed);
+ }
+
+ Notification notification = new NotificationCompat.Builder(context)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(subtext))
+ .setContentIntent(contentIntent)
+ .setContentTitle(title)
+ .setTicker(title)
+ .setSmallIcon(R.drawable.stat_notify_sync_error)
+ .setOngoing(false)
+ .setAutoCancel(true)
+ .build();
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification);
+ }
+
+
+ /**
+ * Starts the FlattrClickWorker as an AsyncTask.
+ */
+ @TargetApi(11)
+ public void executeAsync() {
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ execute();
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java
new file mode 100644
index 000000000..c4aa76ac7
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java
@@ -0,0 +1,47 @@
+package de.danoeh.antennapod.core.asynctask;
+
+import android.content.Context;
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import org.shredzone.flattr4j.exception.FlattrException;
+import org.shredzone.flattr4j.model.Flattr;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Fetch list of flattred things and flattr status in database in a background thread.
+ */
+
+public class FlattrStatusFetcher extends Thread {
+ protected static final String TAG = "FlattrStatusFetcher";
+ protected Context context;
+
+ public FlattrStatusFetcher(Context context) {
+ super();
+ this.context = context;
+ }
+
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status");
+
+ Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
+
+ try {
+ List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings();
+ DBWriter.setFlattredStatus(context, flattredThings).get();
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status");
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java
new file mode 100644
index 000000000..2513d1abd
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java
@@ -0,0 +1,92 @@
+package de.danoeh.antennapod.core.asynctask;
+
+
+import android.annotation.SuppressLint;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.shredzone.flattr4j.exception.FlattrException;
+import org.shredzone.flattr4j.oauth.AccessToken;
+import org.shredzone.flattr4j.oauth.AndroidAuthenticator;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+
+/**
+ * Fetches the access token in the background in order to avoid networkOnMainThread exception.
+ */
+
+public class FlattrTokenFetcher extends AsyncTask<Void, Void, AccessToken> {
+ private static final String TAG = "FlattrTokenFetcher";
+ Context context;
+ AndroidAuthenticator auth;
+ AccessToken token;
+ Uri uri;
+ ProgressDialog dialog;
+ FlattrException exception;
+
+ public FlattrTokenFetcher(Context context, AndroidAuthenticator auth, Uri uri) {
+ super();
+ this.context = context;
+ this.auth = auth;
+ this.uri = uri;
+ }
+
+ @Override
+ protected void onPostExecute(AccessToken result) {
+ if (result != null) {
+ FlattrUtils.storeToken(result);
+ }
+ dialog.dismiss();
+ if (exception == null) {
+ ClientConfig.flattrCallbacks.handleFlattrAuthenticationSuccess(result);
+ } else {
+ FlattrUtils.showErrorDialog(context, exception.getMessage());
+ }
+ }
+
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ dialog = new ProgressDialog(context);
+ dialog.setMessage(context.getString(R.string.processing_label));
+ dialog.setIndeterminate(true);
+ dialog.setCancelable(false);
+ dialog.show();
+ }
+
+
+ @Override
+ protected AccessToken doInBackground(Void... params) {
+ try {
+ token = auth.fetchAccessToken(uri);
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ exception = e;
+ return null;
+ }
+ if (token != null) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Successfully got token");
+ return token;
+ } else {
+ Log.w(TAG, "Flattr token was null");
+ return null;
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public void executeAsync() {
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ execute();
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java
new file mode 100644
index 000000000..c0d8049db
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java
@@ -0,0 +1,37 @@
+package de.danoeh.antennapod.core.asynctask;
+
+import android.net.Uri;
+
+/**
+ * Classes that implement this interface provide access to an image resource that can
+ * be loaded by the Picasso library.
+ */
+public interface PicassoImageResource {
+
+ /**
+ * This scheme should be used by PicassoImageResources to
+ * indicate that the image Uri points to a file that is not an image
+ * (e.g. a media file). This workaround is needed so that the Picasso library
+ * loads these Uri with a Downloader instead of trying to load it directly.
+ * <p/>
+ * For example implementations, see FeedMedia or ExternalMedia.
+ */
+ public static final String SCHEME_MEDIA = "media";
+
+
+ /**
+ * Parameter key for an encoded fallback Uri. This Uri MUST point to a local image file
+ */
+ public static final String PARAM_FALLBACK = "fallback";
+
+ /**
+ * Returns a Uri to the image or null if no image is available.
+ * <p/>
+ * The Uri can either be an HTTP-URL, a URL pointing to a local image file or
+ * a non-image file (see SCHEME_MEDIA for more details).
+ * <p/>
+ * The Uri can also have an optional fallback-URL if loading the default URL
+ * failed (see PARAM_FALLBACK).
+ */
+ public Uri getImageUri();
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java
new file mode 100644
index 000000000..6ace92800
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java
@@ -0,0 +1,152 @@
+package de.danoeh.antennapod.core.asynctask;
+
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.squareup.picasso.Cache;
+import com.squareup.picasso.Downloader;
+import com.squareup.picasso.LruCache;
+import com.squareup.picasso.OkHttpDownloader;
+import com.squareup.picasso.Picasso;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Provides access to Picasso instances.
+ */
+public class PicassoProvider {
+ private static final String TAG = "PicassoProvider";
+
+ private static final boolean DEBUG = false;
+
+ private static ExecutorService executorService;
+ private static Cache memoryCache;
+
+ private static Picasso defaultPicassoInstance;
+ private static Picasso mediaMetadataPicassoInstance;
+
+ private static synchronized ExecutorService getExecutorService() {
+ if (executorService == null) {
+ executorService = Executors.newFixedThreadPool(3);
+ }
+ return executorService;
+ }
+
+ private static synchronized Cache getMemoryCache(Context context) {
+ if (memoryCache == null) {
+ memoryCache = new LruCache(context);
+ }
+ return memoryCache;
+ }
+
+ /**
+ * Returns a Picasso instance that uses an OkHttpDownloader. This instance can only load images
+ * from image files.
+ * <p/>
+ * This instance should be used as long as no images from media files are loaded.
+ */
+ public static synchronized Picasso getDefaultPicassoInstance(Context context) {
+ Validate.notNull(context);
+ if (defaultPicassoInstance == null) {
+ defaultPicassoInstance = new Picasso.Builder(context)
+ .indicatorsEnabled(DEBUG)
+ .loggingEnabled(DEBUG)
+ .downloader(new OkHttpDownloader(context))
+ .executor(getExecutorService())
+ .memoryCache(getMemoryCache(context))
+ .listener(new Picasso.Listener() {
+ @Override
+ public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) {
+ Log.e(TAG, "Failed to load Uri:" + uri.toString());
+ e.printStackTrace();
+ }
+ })
+ .build();
+ }
+ return defaultPicassoInstance;
+ }
+
+ /**
+ * Returns a Picasso instance that uses a MediaMetadataRetriever if the given Uri is a media file
+ * and a default OkHttpDownloader otherwise.
+ */
+ public static synchronized Picasso getMediaMetadataPicassoInstance(Context context) {
+ Validate.notNull(context);
+ if (mediaMetadataPicassoInstance == null) {
+ mediaMetadataPicassoInstance = new Picasso.Builder(context)
+ .indicatorsEnabled(DEBUG)
+ .loggingEnabled(DEBUG)
+ .downloader(new MediaMetadataDownloader(context))
+ .executor(getExecutorService())
+ .memoryCache(getMemoryCache(context))
+ .listener(new Picasso.Listener() {
+ @Override
+ public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) {
+ Log.e(TAG, "Failed to load Uri:" + uri.toString());
+ e.printStackTrace();
+ }
+ })
+ .build();
+ }
+ return mediaMetadataPicassoInstance;
+ }
+
+ private static class MediaMetadataDownloader implements Downloader {
+
+ private static final String TAG = "MediaMetadataDownloader";
+
+ private final OkHttpDownloader okHttpDownloader;
+
+ public MediaMetadataDownloader(Context context) {
+ Validate.notNull(context);
+ okHttpDownloader = new OkHttpDownloader(context);
+ }
+
+ @Override
+ public Response load(Uri uri, boolean b) throws IOException {
+ if (StringUtils.equals(uri.getScheme(), PicassoImageResource.SCHEME_MEDIA)) {
+ String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(uri.getLastPathSegment()));
+ if (StringUtils.startsWith(type, "image")) {
+ File imageFile = new File(uri.toString());
+ return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length());
+ } else {
+ MediaMetadataRetriever mmr = new MediaMetadataRetriever();
+ mmr.setDataSource(uri.getPath());
+ byte[] data = mmr.getEmbeddedPicture();
+ mmr.release();
+
+ if (data != null) {
+ return new Response(new ByteArrayInputStream(data), true, data.length);
+ } else {
+
+ // check for fallback Uri
+ String fallbackParam = uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK);
+
+ if (fallbackParam != null) {
+ String fallback = Uri.decode(Uri.parse(fallbackParam).getPath());
+ if (fallback != null) {
+ File imageFile = new File(fallback);
+ return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length());
+ }
+ }
+ return null;
+ }
+ }
+ }
+ return okHttpDownloader.load(uri, b);
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
new file mode 100644
index 000000000..1535e2e9a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java
@@ -0,0 +1,211 @@
+package de.danoeh.antennapod.core.backup;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInputStream;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.BackupHelper;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.math.BigInteger;
+import java.security.DigestInputStream;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.opml.OpmlElement;
+import de.danoeh.antennapod.core.opml.OpmlReader;
+import de.danoeh.antennapod.core.opml.OpmlWriter;
+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.LangUtils;
+
+public class OpmlBackupAgent extends BackupAgentHelper {
+ private static final String OPML_BACKUP_KEY = "opml";
+
+ @Override
+ public void onCreate() {
+ addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this));
+ }
+
+ private static final void LOGD(String tag, String msg) {
+ if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, msg);
+ }
+ }
+
+ private static final void LOGD(String tag, String msg, Throwable tr) {
+ if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, msg, tr);
+ }
+ }
+
+ /** Class for backing up and restoring the OPML file. */
+ private static class OpmlBackupHelper implements BackupHelper {
+ private static final String TAG = "OpmlBackupHelper";
+
+ private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml";
+
+ private final Context mContext;
+
+ /** Checksum of restored OPML file */
+ private byte[] mChecksum;
+
+ public OpmlBackupHelper(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
+ Log.d(TAG, "Performing backup");
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ MessageDigest digester = null;
+ Writer writer;
+
+ try {
+ digester = MessageDigest.getInstance("MD5");
+ writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester),
+ LangUtils.UTF_8);
+ } catch (NoSuchAlgorithmException e) {
+ writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8);
+ }
+
+ try {
+ // Write OPML
+ new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer);
+
+ // Compare checksum of new and old file to see if we need to perform a backup at all
+ if (digester != null) {
+ byte[] newChecksum = digester.digest();
+ LOGD(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16));
+
+ // Get the old checksum
+ if (oldState != null) {
+ FileInputStream inState = new FileInputStream(oldState.getFileDescriptor());
+ int len = inState.read();
+
+ if (len != -1) {
+ byte[] oldChecksum = new byte[len];
+ inState.read(oldChecksum);
+ LOGD(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16));
+
+ if (Arrays.equals(oldChecksum, newChecksum)) {
+ LOGD(TAG, "Checksums are the same; won't backup");
+ return;
+ }
+ }
+ }
+
+ writeNewStateDescription(newState, newChecksum);
+ }
+
+ LOGD(TAG, "Backing up OPML");
+ byte[] bytes = byteStream.toByteArray();
+ data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length);
+ data.writeEntityData(bytes, bytes.length);
+ } catch (IOException e) {
+ Log.e(TAG, "Error during backup", e);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ @Override
+ public void restoreEntity(BackupDataInputStream data) {
+ LOGD(TAG, "Backup restore");
+
+ if (!OPML_ENTITY_KEY.equals(data.getKey())) {
+ LOGD(TAG, "Unknown entity key: " + data.getKey());
+ return;
+ }
+
+ MessageDigest digester = null;
+ Reader reader;
+
+ try {
+ digester = MessageDigest.getInstance("MD5");
+ reader = new InputStreamReader(new DigestInputStream(data, digester),
+ LangUtils.UTF_8);
+ } catch (NoSuchAlgorithmException e) {
+ reader = new InputStreamReader(data, LangUtils.UTF_8);
+ }
+
+ try {
+ ArrayList<OpmlElement> opmlElements = new OpmlReader().readDocument(reader);
+ mChecksum = digester == null ? null : digester.digest();
+ DownloadRequester downloader = DownloadRequester.getInstance();
+ Date lastUpdated = new Date();
+
+ for (OpmlElement opmlElem : opmlElements) {
+ Feed feed = new Feed(opmlElem.getXmlUrl(), lastUpdated, opmlElem.getText());
+
+ try {
+ downloader.downloadFeed(mContext, feed);
+ } catch (DownloadRequestException e) {
+ LOGD(TAG, "Error while restoring/downloading feed", e);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Error while parsing the OPML file", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to restore OPML backup", e);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ @Override
+ public void writeNewStateDescription(ParcelFileDescriptor newState) {
+ writeNewStateDescription(newState, mChecksum);
+ }
+
+ /**
+ * Writes the new state description, which is the checksum of the OPML file.
+ *
+ * @param newState
+ * @param checksum
+ */
+ private void writeNewStateDescription(ParcelFileDescriptor newState, byte[] checksum) {
+ if (checksum == null) {
+ return;
+ }
+
+ try {
+ FileOutputStream outState = new FileOutputStream(newState.getFileDescriptor());
+ outState.write(checksum.length);
+ outState.write(checksum);
+ outState.flush();
+ outState.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write new state description", e);
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java
new file mode 100644
index 000000000..ba1add895
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java
@@ -0,0 +1,64 @@
+package de.danoeh.antennapod.core.dialog;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.R;
+
+/**
+ * Creates an AlertDialog which asks the user to confirm something. Other
+ * classes can handle events like confirmation or cancellation.
+ */
+public abstract class ConfirmationDialog {
+ private static final String TAG = "ConfirmationDialog";
+
+ Context context;
+ int titleId;
+ int messageId;
+
+ public ConfirmationDialog(Context context, int titleId, int messageId) {
+ this.context = context;
+ this.titleId = titleId;
+ this.messageId = messageId;
+ }
+
+ public void onCancelButtonPressed(DialogInterface dialog) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Dialog was cancelled");
+ dialog.dismiss();
+ }
+
+ public abstract void onConfirmButtonPressed(DialogInterface dialog);
+
+ public final AlertDialog createNewDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(titleId);
+ builder.setMessage(messageId);
+ builder.setPositiveButton(R.string.confirm_label,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ onConfirmButtonPressed(dialog);
+ }
+ });
+ builder.setNegativeButton(R.string.cancel_label,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ onCancelButtonPressed(dialog);
+ }
+ });
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onCancelButtonPressed(dialog);
+ }
+ });
+ return builder.create();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java
new file mode 100644
index 000000000..3d174bd8e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java
@@ -0,0 +1,30 @@
+package de.danoeh.antennapod.core.dialog;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import de.danoeh.antennapod.core.R;
+
+/** Creates Alert Dialogs if a DownloadRequestException has happened. */
+public class DownloadRequestErrorDialogCreator {
+ private DownloadRequestErrorDialogCreator() {
+ }
+
+ public static void newRequestErrorDialog(Context context,
+ String errorMessage) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setNeutralButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .setTitle(R.string.download_error_request_error)
+ .setMessage(
+ context.getString(R.string.download_request_error_dialog_message_prefix)
+ + errorMessage);
+ builder.create().show();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java
new file mode 100644
index 000000000..ce3352ed6
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.core.feed;
+
+public abstract class Chapter extends FeedComponent {
+
+ /** Defines starting point in milliseconds. */
+ protected long start;
+ protected String title;
+ protected String link;
+
+ public Chapter() {
+ }
+
+ public Chapter(long start) {
+ super();
+ this.start = start;
+ }
+
+ public Chapter(long start, String title, FeedItem item, String link) {
+ super();
+ this.start = start;
+ this.title = title;
+ this.link = link;
+ }
+
+ public abstract int getChapterType();
+
+ public long getStart() {
+ return start;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ return title;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java
new file mode 100644
index 000000000..f8815dcf0
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java
@@ -0,0 +1,140 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.os.Handler;
+import android.util.Log;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.core.BuildConfig;
+
+import java.util.AbstractQueue;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * Notifies its observers about changes in the feed database. Observers can
+ * register by retrieving an instance of this class and registering an
+ * EventListener. When new events arrive, the EventDistributor will process the
+ * event queue in a handler that runs on the main thread. The observers will only
+ * be notified once if the event queue contains multiple elements.
+ *
+ * Events can be sent with the send* methods.
+ */
+public class EventDistributor extends Observable {
+ private static final String TAG = "EventDistributor";
+
+ public static final int FEED_LIST_UPDATE = 1;
+ public static final int UNREAD_ITEMS_UPDATE = 2;
+ public static final int QUEUE_UPDATE = 4;
+ public static final int DOWNLOADLOG_UPDATE = 8;
+ public static final int PLAYBACK_HISTORY_UPDATE = 16;
+ public static final int DOWNLOAD_QUEUED = 32;
+ public static final int DOWNLOAD_HANDLED = 64;
+
+ private Handler handler;
+ private AbstractQueue<Integer> events;
+
+ private static EventDistributor instance;
+
+ private EventDistributor() {
+ this.handler = new Handler();
+ events = new ConcurrentLinkedQueue<Integer>();
+ }
+
+ public static synchronized EventDistributor getInstance() {
+ if (instance == null) {
+ instance = new EventDistributor();
+ }
+ return instance;
+ }
+
+ public void register(EventListener el) {
+ addObserver(el);
+ }
+
+ public void unregister(EventListener el) {
+ deleteObserver(el);
+ }
+
+ public void addEvent(Integer i) {
+ events.offer(i);
+ handler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ processEventQueue();
+ }
+ });
+ }
+
+ private void processEventQueue() {
+ Integer result = 0;
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Processing event queue. Number of events: "
+ + events.size());
+ for (Integer current = events.poll(); current != null; current = events
+ .poll()) {
+ result |= current;
+ }
+ if (result != 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Notifying observers. Data: " + result);
+ setChanged();
+ notifyObservers(result);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Event queue didn't contain any new events. Observers will not be notified.");
+ }
+ }
+
+ @Override
+ public void addObserver(Observer observer) {
+ super.addObserver(observer);
+ Validate.isInstanceOf(EventListener.class, observer);
+ }
+
+ public void sendDownloadQueuedBroadcast() {
+ addEvent(DOWNLOAD_QUEUED);
+ }
+
+ public void sendUnreadItemsUpdateBroadcast() {
+ addEvent(UNREAD_ITEMS_UPDATE);
+ }
+
+ public void sendQueueUpdateBroadcast() {
+ addEvent(QUEUE_UPDATE);
+ }
+
+ public void sendFeedUpdateBroadcast() {
+ addEvent(FEED_LIST_UPDATE);
+ }
+
+ public void sendPlaybackHistoryUpdateBroadcast() {
+ addEvent(PLAYBACK_HISTORY_UPDATE);
+ }
+
+ public void sendDownloadLogUpdateBroadcast() {
+ addEvent(DOWNLOADLOG_UPDATE);
+ }
+
+ public void sendDownloadHandledBroadcast() {
+ addEvent(DOWNLOAD_HANDLED);
+ }
+
+ public static abstract class EventListener implements Observer {
+
+ @Override
+ public void update(Observable observable, Object data) {
+ if (observable instanceof EventDistributor
+ && data instanceof Integer) {
+ update((EventDistributor) observable, (Integer) data);
+ }
+ }
+
+ public abstract void update(EventDistributor eventDistributor,
+ Integer arg);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
new file mode 100644
index 000000000..3f83ab8b6
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
@@ -0,0 +1,445 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.content.Context;
+import android.net.Uri;
+
+import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.EpisodeFilter;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+import de.danoeh.antennapod.core.util.flattr.FlattrThing;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Data Object for a whole feed
+ *
+ * @author daniel
+ */
+public class Feed extends FeedFile implements FlattrThing, PicassoImageResource {
+ public static final int FEEDFILETYPE_FEED = 0;
+ public static final String TYPE_RSS2 = "rss";
+ public static final String TYPE_RSS091 = "rss";
+ public static final String TYPE_ATOM1 = "atom";
+
+ private String title;
+ /**
+ * Contains 'id'-element in Atom feed.
+ */
+ private String feedIdentifier;
+ /**
+ * Link to the website.
+ */
+ private String link;
+ private String description;
+ private String language;
+ /**
+ * Name of the author
+ */
+ private String author;
+ private FeedImage image;
+ private List<FeedItem> items;
+ /**
+ * Date of last refresh.
+ */
+ private Date lastUpdate;
+ private FlattrStatus flattrStatus;
+ private String paymentLink;
+ /**
+ * Feed type, for example RSS 2 or Atom
+ */
+ private String type;
+
+ /**
+ * Feed preferences
+ */
+ private FeedPreferences preferences;
+
+ /**
+ * This constructor is used for restoring a feed from the database.
+ */
+ public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink,
+ String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
+ String downloadUrl, boolean downloaded, FlattrStatus status) {
+ super(fileUrl, downloadUrl, downloaded);
+ this.id = id;
+ this.title = title;
+ if (lastUpdate != null) {
+ this.lastUpdate = (Date) lastUpdate.clone();
+ } else {
+ this.lastUpdate = null;
+ }
+ this.link = link;
+ this.description = description;
+ this.paymentLink = paymentLink;
+ this.author = author;
+ this.language = language;
+ this.type = type;
+ this.feedIdentifier = feedIdentifier;
+ this.image = image;
+ this.flattrStatus = status;
+
+ items = new ArrayList<FeedItem>();
+ }
+
+ /**
+ * This constructor is used for test purposes and uses a default flattr status object.
+ */
+ public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink,
+ String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
+ String downloadUrl, boolean downloaded) {
+ this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image,
+ fileUrl, downloadUrl, downloaded, new FlattrStatus());
+ }
+
+ /**
+ * This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized.
+ */
+ public Feed() {
+ super();
+ items = new ArrayList<FeedItem>();
+ lastUpdate = new Date();
+ this.flattrStatus = new FlattrStatus();
+ }
+
+ /**
+ * This constructor is used for requesting a feed download (it must not be used for anything else!). It should NOT be
+ * used if the title of the feed is already known.
+ */
+ public Feed(String url, Date lastUpdate) {
+ super(null, url, false);
+ this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
+ this.flattrStatus = new FlattrStatus();
+ }
+
+ /**
+ * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be
+ * used if the title of the feed is already known.
+ */
+ public Feed(String url, Date lastUpdate, String title) {
+ this(url, lastUpdate);
+ this.title = title;
+ this.flattrStatus = new FlattrStatus();
+ }
+
+ /**
+ * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be
+ * used if the title of the feed is already known.
+ */
+ public Feed(String url, Date lastUpdate, String title, String username, String password) {
+ this(url, lastUpdate, title);
+ preferences = new FeedPreferences(0, true, username, password);
+ }
+
+ /**
+ * Returns the number of FeedItems where 'read' is false. If the 'display
+ * only episodes' - preference is set to true, this method will only count
+ * items with episodes.
+ */
+ public int getNumOfNewItems() {
+ int count = 0;
+ for (FeedItem item : items) {
+ if (item.getState() == FeedItem.State.NEW) {
+ if (!UserPreferences.isDisplayOnlyEpisodes()
+ || item.getMedia() != null) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Returns the number of FeedItems where the media started to play but
+ * wasn't finished yet.
+ */
+ public int getNumOfStartedItems() {
+ int count = 0;
+
+ for (FeedItem item : items) {
+ FeedItem.State state = item.getState();
+ if (state == FeedItem.State.IN_PROGRESS
+ || state == FeedItem.State.PLAYING) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Returns true if at least one item in the itemlist is unread.
+ *
+ * @param enableEpisodeFilter true if this method should only count items with episodes if
+ * the 'display only episodes' - preference is set to true by the
+ * user.
+ */
+ public boolean hasNewItems(boolean enableEpisodeFilter) {
+ for (FeedItem item : items) {
+ if (item.getState() == FeedItem.State.NEW) {
+ if (!(enableEpisodeFilter && UserPreferences
+ .isDisplayOnlyEpisodes()) || item.getMedia() != null) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the number of FeedItems.
+ *
+ * @param enableEpisodeFilter true if this method should only count items with episodes if
+ * the 'display only episodes' - preference is set to true by the
+ * user.
+ */
+ public int getNumOfItems(boolean enableEpisodeFilter) {
+ if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) {
+ return EpisodeFilter.countItemsWithEpisodes(items);
+ } else {
+ return items.size();
+ }
+ }
+
+ /**
+ * Returns the item at the specified index.
+ *
+ * @param enableEpisodeFilter true if this method should ignore items without episdodes if
+ * the episodes filter has been enabled by the user.
+ */
+ public FeedItem getItemAtIndex(boolean enableEpisodeFilter, int position) {
+ if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) {
+ return EpisodeFilter.accessEpisodeByIndex(items, position);
+ } else {
+ return items.get(position);
+ }
+ }
+
+ /**
+ * Returns the value that uniquely identifies this Feed. If the
+ * feedIdentifier attribute is not null, it will be returned. Else it will
+ * try to return the title. If the title is not given, it will use the link
+ * of the feed.
+ */
+ public String getIdentifyingValue() {
+ if (feedIdentifier != null && !feedIdentifier.isEmpty()) {
+ return feedIdentifier;
+ } else if (download_url != null && !download_url.isEmpty()) {
+ return download_url;
+ } else if (title != null && !title.isEmpty()) {
+ return title;
+ } else {
+ return link;
+ }
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ if (title != null) {
+ return title;
+ } else {
+ return download_url;
+ }
+ }
+
+ public void updateFromOther(Feed other) {
+ super.updateFromOther(other);
+ if (other.title != null) {
+ title = other.title;
+ }
+ if (other.feedIdentifier != null) {
+ feedIdentifier = other.feedIdentifier;
+ }
+ if (other.link != null) {
+ link = other.link;
+ }
+ if (other.description != null) {
+ description = other.description;
+ }
+ if (other.language != null) {
+ language = other.language;
+ }
+ if (other.author != null) {
+ author = other.author;
+ }
+ if (other.paymentLink != null) {
+ paymentLink = other.paymentLink;
+ }
+ if (other.flattrStatus != null) {
+ flattrStatus = other.flattrStatus;
+ }
+ }
+
+ public boolean compareWithOther(Feed other) {
+ if (super.compareWithOther(other)) {
+ return true;
+ }
+ if (!title.equals(other.title)) {
+ return true;
+ }
+ if (other.feedIdentifier != null) {
+ if (feedIdentifier == null
+ || !feedIdentifier.equals(other.feedIdentifier)) {
+ return true;
+ }
+ }
+ if (other.link != null) {
+ if (link == null || !link.equals(other.link)) {
+ return true;
+ }
+ }
+ if (other.description != null) {
+ if (description == null || !description.equals(other.description)) {
+ return true;
+ }
+ }
+ if (other.language != null) {
+ if (language == null || !language.equals(other.language)) {
+ return true;
+ }
+ }
+ if (other.author != null) {
+ if (author == null || !author.equals(other.author)) {
+ return true;
+ }
+ }
+ if (other.paymentLink != null) {
+ if (paymentLink == null || !paymentLink.equals(other.paymentLink)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getTypeAsInt() {
+ return FEEDFILETYPE_FEED;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public FeedImage getImage() {
+ return image;
+ }
+
+ public void setImage(FeedImage image) {
+ this.image = image;
+ }
+
+ public List<FeedItem> getItems() {
+ return items;
+ }
+
+ public void setItems(List<FeedItem> list) {
+ this.items = list;
+ }
+
+ public Date getLastUpdate() {
+ return (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
+ }
+
+ public void setLastUpdate(Date lastUpdate) {
+ this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
+ }
+
+ public String getFeedIdentifier() {
+ return feedIdentifier;
+ }
+
+ public void setFeedIdentifier(String feedIdentifier) {
+ this.feedIdentifier = feedIdentifier;
+ }
+
+ public void setFlattrStatus(FlattrStatus status) {
+ this.flattrStatus = status;
+ }
+
+ public FlattrStatus getFlattrStatus() {
+ return flattrStatus;
+ }
+
+ public String getPaymentLink() {
+ return paymentLink;
+ }
+
+ public void setPaymentLink(String paymentLink) {
+ this.paymentLink = paymentLink;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public void setPreferences(FeedPreferences preferences) {
+ this.preferences = preferences;
+ }
+
+ public FeedPreferences getPreferences() {
+ return preferences;
+ }
+
+ public void savePreferences(Context context) {
+ DBWriter.setFeedPreferences(context, preferences);
+ }
+
+ @Override
+ public void setId(long id) {
+ super.setId(id);
+ if (preferences != null) {
+ preferences.setFeedID(id);
+ }
+ }
+
+ @Override
+ public Uri getImageUri() {
+ if (image != null) {
+ return image.getImageUri();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java
new file mode 100644
index 000000000..05115c1ea
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java
@@ -0,0 +1,66 @@
+package de.danoeh.antennapod.core.feed;
+
+/**
+ * Represents every possible component of a feed
+ *
+ * @author daniel
+ */
+public abstract class FeedComponent {
+
+ protected long id;
+
+ public FeedComponent() {
+ super();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ /**
+ * Update this FeedComponent's attributes with the attributes from another
+ * FeedComponent. This method should only update attributes which where read from
+ * the feed.
+ */
+ public void updateFromOther(FeedComponent other) {
+ }
+
+ /**
+ * Compare's this FeedComponent's attribute values with another FeedComponent's
+ * attribute values. This method will only compare attributes which were
+ * read from the feed.
+ *
+ * @return true if attribute values are different, false otherwise
+ */
+ public boolean compareWithOther(FeedComponent other) {
+ return false;
+ }
+
+
+ /**
+ * Should return a non-null, human-readable String so that the item can be
+ * identified by the user. Can be title, download-url, etc.
+ */
+ public abstract String getHumanReadableIdentifier();
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FeedComponent that = (FeedComponent) o;
+
+ if (id != that.id) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (id ^ (id >>> 32));
+ }
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java
new file mode 100644
index 000000000..3dc58654b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java
@@ -0,0 +1,105 @@
+package de.danoeh.antennapod.core.feed;
+
+import java.io.File;
+
+/**
+ * Represents a component of a Feed that has to be downloaded
+ */
+public abstract class FeedFile extends FeedComponent {
+
+ protected String file_url;
+ protected String download_url;
+ protected boolean downloaded;
+
+ /**
+ * Creates a new FeedFile object.
+ *
+ * @param file_url The location of the FeedFile. If this is null, the downloaded-attribute
+ * will automatically be set to false.
+ * @param download_url The location where the FeedFile can be downloaded.
+ * @param downloaded true if the FeedFile has been downloaded, false otherwise. This parameter
+ * will automatically be interpreted as false if the file_url is null.
+ */
+ public FeedFile(String file_url, String download_url, boolean downloaded) {
+ super();
+ this.file_url = file_url;
+ this.download_url = download_url;
+ this.downloaded = (file_url != null) && downloaded;
+ }
+
+ public FeedFile() {
+ this(null, null, false);
+ }
+
+ public abstract int getTypeAsInt();
+
+ /**
+ * Update this FeedFile's attributes with the attributes from another
+ * FeedFile. This method should only update attributes which where read from
+ * the feed.
+ */
+ public void updateFromOther(FeedFile other) {
+ super.updateFromOther(other);
+ this.download_url = other.download_url;
+ }
+
+ /**
+ * Compare's this FeedFile's attribute values with another FeedFile's
+ * attribute values. This method will only compare attributes which were
+ * read from the feed.
+ *
+ * @return true if attribute values are different, false otherwise
+ */
+ public boolean compareWithOther(FeedFile other) {
+ if (super.compareWithOther(other)) {
+ return true;
+ }
+ if (!download_url.equals(other.download_url)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the file exists at file_url.
+ */
+ public boolean fileExists() {
+ if (file_url == null) {
+ return false;
+ } else {
+ File f = new File(file_url);
+ return f.exists();
+ }
+ }
+
+ public String getFile_url() {
+ return file_url;
+ }
+
+ /**
+ * Changes the file_url of this FeedFile. Setting this value to
+ * null will also set the downloaded-attribute to false.
+ */
+ public void setFile_url(String file_url) {
+ this.file_url = file_url;
+ if (file_url == null) {
+ downloaded = false;
+ }
+ }
+
+ public String getDownload_url() {
+ return download_url;
+ }
+
+ public void setDownload_url(String download_url) {
+ this.download_url = download_url;
+ }
+
+ public boolean isDownloaded() {
+ return downloaded;
+ }
+
+ public void setDownloaded(boolean downloaded) {
+ this.downloaded = downloaded;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java
new file mode 100644
index 000000000..b01747f7f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java
@@ -0,0 +1,72 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.net.Uri;
+
+import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+
+import java.io.File;
+
+
+public class FeedImage extends FeedFile implements PicassoImageResource {
+ public static final int FEEDFILETYPE_FEEDIMAGE = 1;
+
+ protected String title;
+ protected FeedComponent owner;
+
+ public FeedImage(FeedComponent owner, String download_url, String title) {
+ super(null, download_url, false);
+ this.download_url = download_url;
+ this.title = title;
+ this.owner = owner;
+ }
+
+ public FeedImage(long id, String title, String file_url,
+ String download_url, boolean downloaded) {
+ super(file_url, download_url, downloaded);
+ this.id = id;
+ this.title = title;
+ }
+
+ public FeedImage() {
+ super();
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ if (owner != null && owner.getHumanReadableIdentifier() != null) {
+ return owner.getHumanReadableIdentifier();
+ } else {
+ return download_url;
+ }
+ }
+
+ @Override
+ public int getTypeAsInt() {
+ return FEEDFILETYPE_FEEDIMAGE;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public FeedComponent getOwner() {
+ return owner;
+ }
+
+ public void setOwner(FeedComponent owner) {
+ this.owner = owner;
+ }
+
+ @Override
+ public Uri getImageUri() {
+ if (file_url != null && downloaded) {
+ return Uri.fromFile(new File(file_url));
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
new file mode 100644
index 000000000..d056917e1
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
@@ -0,0 +1,334 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.net.Uri;
+
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.ShownotesProvider;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+import de.danoeh.antennapod.core.util.flattr.FlattrThing;
+
+/**
+ * Data Object for a XML message
+ *
+ * @author daniel
+ */
+public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource {
+
+ /**
+ * The id/guid that can be found in the rss/atom feed. Might not be set.
+ */
+ private String itemIdentifier;
+ private String title;
+ /**
+ * The description of a feeditem.
+ */
+ private String description;
+ /**
+ * The content of the content-encoded tag of a feeditem.
+ */
+ private String contentEncoded;
+
+ private String link;
+ private Date pubDate;
+ private FeedMedia media;
+
+ private Feed feed;
+ private long feedId;
+
+ private boolean read;
+ private String paymentLink;
+ private FlattrStatus flattrStatus;
+ private List<Chapter> chapters;
+ private FeedImage image;
+
+ public FeedItem() {
+ this.read = true;
+ this.flattrStatus = new FlattrStatus();
+ }
+
+ /**
+ * This constructor should be used for creating test objects.
+ */
+ public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed) {
+ this.id = id;
+ this.title = title;
+ this.itemIdentifier = itemIdentifier;
+ this.link = link;
+ this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
+ this.read = read;
+ this.feed = feed;
+ this.flattrStatus = new FlattrStatus();
+ }
+
+ public void updateFromOther(FeedItem other) {
+ super.updateFromOther(other);
+ if (other.title != null) {
+ title = other.title;
+ }
+ if (other.getDescription() != null) {
+ description = other.getDescription();
+ }
+ if (other.getContentEncoded() != null) {
+ contentEncoded = other.contentEncoded;
+ }
+ if (other.link != null) {
+ link = other.link;
+ }
+ if (other.pubDate != null && other.pubDate != pubDate) {
+ pubDate = other.pubDate;
+ }
+ if (other.media != null) {
+ if (media == null) {
+ setMedia(other.media);
+ } else if (media.compareWithOther(other)) {
+ media.updateFromOther(other);
+ }
+ }
+ if (other.paymentLink != null) {
+ paymentLink = other.paymentLink;
+ }
+ if (other.chapters != null) {
+ if (chapters == null) {
+ chapters = other.chapters;
+ }
+ }
+ if (image == null) {
+ image = other.image;
+ }
+ }
+
+ /**
+ * Returns the value that uniquely identifies this FeedItem. If the
+ * itemIdentifier attribute is not null, it will be returned. Else it will
+ * try to return the title. If the title is not given, it will use the link
+ * of the entry.
+ */
+ public String getIdentifyingValue() {
+ if (itemIdentifier != null && !itemIdentifier.isEmpty()) {
+ return itemIdentifier;
+ } else if (title != null && !title.isEmpty()) {
+ return title;
+ } else {
+ return link;
+ }
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public Date getPubDate() {
+ if (pubDate != null) {
+ return (Date) pubDate.clone();
+ } else {
+ return null;
+ }
+ }
+
+ public void setPubDate(Date pubDate) {
+ if (pubDate != null) {
+ this.pubDate = (Date) pubDate.clone();
+ } else {
+ this.pubDate = null;
+ }
+ }
+
+ public FeedMedia getMedia() {
+ return media;
+ }
+
+ /**
+ * Sets the media object of this FeedItem. If the given
+ * FeedMedia object is not null, it's 'item'-attribute value
+ * will also be set to this item.
+ */
+ public void setMedia(FeedMedia media) {
+ this.media = media;
+ if (media != null && media.getItem() != this) {
+ media.setItem(this);
+ }
+ }
+
+ public Feed getFeed() {
+ return feed;
+ }
+
+ public void setFeed(Feed feed) {
+ this.feed = feed;
+ }
+
+ public boolean isRead() {
+ return read || isInProgress();
+ }
+
+ public void setRead(boolean read) {
+ this.read = read;
+ }
+
+ private boolean isInProgress() {
+ return (media != null && media.isInProgress());
+ }
+
+ public String getContentEncoded() {
+ return contentEncoded;
+ }
+
+ public void setContentEncoded(String contentEncoded) {
+ this.contentEncoded = contentEncoded;
+ }
+
+ public void setFlattrStatus(FlattrStatus status) {
+ this.flattrStatus = status;
+ }
+
+ public FlattrStatus getFlattrStatus() {
+ return flattrStatus;
+ }
+
+ public String getPaymentLink() {
+ return paymentLink;
+ }
+
+ public void setPaymentLink(String paymentLink) {
+ this.paymentLink = paymentLink;
+ }
+
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+ public void setChapters(List<Chapter> chapters) {
+ this.chapters = chapters;
+ }
+
+ public String getItemIdentifier() {
+ return itemIdentifier;
+ }
+
+ public void setItemIdentifier(String itemIdentifier) {
+ this.itemIdentifier = itemIdentifier;
+ }
+
+ public boolean hasMedia() {
+ return media != null;
+ }
+
+ private boolean isPlaying() {
+ if (media != null) {
+ return media.isPlaying();
+ }
+ return false;
+ }
+
+ @Override
+ public Callable<String> loadShownotes() {
+ return new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+
+ if (contentEncoded == null || description == null) {
+ DBReader.loadExtraInformationOfFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), FeedItem.this);
+
+ }
+ return (contentEncoded != null) ? contentEncoded : description;
+ }
+ };
+ }
+
+ @Override
+ public Uri getImageUri() {
+ if (hasItemImageDownloaded()) {
+ return image.getImageUri();
+ } else if (hasMedia()) {
+ return media.getImageUri();
+ } else if (feed != null) {
+ return feed.getImageUri();
+ } else {
+ return null;
+ }
+ }
+
+ public enum State {
+ NEW, IN_PROGRESS, READ, PLAYING
+ }
+
+ public State getState() {
+ if (hasMedia()) {
+ if (isPlaying()) {
+ return State.PLAYING;
+ }
+ if (isInProgress()) {
+ return State.IN_PROGRESS;
+ }
+ }
+ return (isRead() ? State.READ : State.NEW);
+ }
+
+ public long getFeedId() {
+ return feedId;
+ }
+
+ public void setFeedId(long feedId) {
+ this.feedId = feedId;
+ }
+
+ /**
+ * Returns the image of this item or the image of the feed if this item does
+ * not have its own image.
+ */
+ public FeedImage getImage() {
+ return (hasItemImage()) ? image : feed.getImage();
+ }
+
+ public void setImage(FeedImage image) {
+ this.image = image;
+ if (image != null) {
+ image.setOwner(this);
+ }
+ }
+
+ /**
+ * Returns true if this FeedItem has its own image, false otherwise.
+ */
+ public boolean hasItemImage() {
+ return image != null;
+ }
+
+ /**
+ * Returns true if this FeedItem has its own image and the image has been downloaded.
+ */
+ public boolean hasItemImageDownloaded() {
+ return image != null && image.isDownloaded();
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ return title;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
new file mode 100644
index 000000000..defcfd598
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
@@ -0,0 +1,412 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.ChapterUtils;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+public class FeedMedia extends FeedFile implements Playable {
+ private static final String TAG = "FeedMedia";
+
+ public static final int FEEDFILETYPE_FEEDMEDIA = 2;
+ public static final int PLAYABLE_TYPE_FEEDMEDIA = 1;
+
+ public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId";
+ public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId";
+
+ private int duration;
+ private int position; // Current position in file
+ private int played_duration; // How many ms of this file have been played (for autoflattring)
+ private long size; // File size in Byte
+ private String mime_type;
+ private volatile FeedItem item;
+ private Date playbackCompletionDate;
+
+ /* Used for loading item when restoring from parcel. */
+ private long itemID;
+
+ public FeedMedia(FeedItem i, String download_url, long size,
+ String mime_type) {
+ super(null, download_url, false);
+ this.item = i;
+ this.size = size;
+ this.mime_type = mime_type;
+ }
+
+ public FeedMedia(long id, FeedItem item, int duration, int position,
+ long size, String mime_type, String file_url, String download_url,
+ boolean downloaded, Date playbackCompletionDate, int played_duration) {
+ super(file_url, download_url, downloaded);
+ this.id = id;
+ this.item = item;
+ this.duration = duration;
+ this.position = position;
+ this.played_duration = played_duration;
+ this.size = size;
+ this.mime_type = mime_type;
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ }
+
+ public FeedMedia(long id, FeedItem item) {
+ super();
+ this.id = id;
+ this.item = item;
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ if (item != null && item.getTitle() != null) {
+ return item.getTitle();
+ } else {
+ return download_url;
+ }
+ }
+
+ /**
+ * Uses mimetype to determine the type of media.
+ */
+ public MediaType getMediaType() {
+ if (mime_type == null || mime_type.isEmpty()) {
+ return MediaType.UNKNOWN;
+ } else {
+ if (mime_type.startsWith("audio")) {
+ return MediaType.AUDIO;
+ } else if (mime_type.startsWith("video")) {
+ return MediaType.VIDEO;
+ } else if (mime_type.equals("application/ogg")) {
+ return MediaType.AUDIO;
+ }
+ }
+ return MediaType.UNKNOWN;
+ }
+
+ public void updateFromOther(FeedMedia other) {
+ super.updateFromOther(other);
+ if (other.size > 0) {
+ size = other.size;
+ }
+ if (other.mime_type != null) {
+ mime_type = other.mime_type;
+ }
+ }
+
+ public boolean compareWithOther(FeedMedia other) {
+ if (super.compareWithOther(other)) {
+ return true;
+ }
+ if (other.mime_type != null) {
+ if (mime_type == null || !mime_type.equals(other.mime_type)) {
+ return true;
+ }
+ }
+ if (other.size > 0 && other.size != size) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Reads playback preferences to determine whether this FeedMedia object is
+ * currently being played.
+ */
+ public boolean isPlaying() {
+ return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA
+ && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id;
+ }
+
+ @Override
+ public int getTypeAsInt() {
+ return FEEDFILETYPE_FEEDMEDIA;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public void setDuration(int duration) {
+ this.duration = duration;
+ }
+
+ public int getPlayedDuration() {
+ return played_duration;
+ }
+
+ public void setPlayedDuration(int played_duration) {
+ this.played_duration = played_duration;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ public String getMime_type() {
+ return mime_type;
+ }
+
+ public void setMime_type(String mime_type) {
+ this.mime_type = mime_type;
+ }
+
+ public FeedItem getItem() {
+ return item;
+ }
+
+ /**
+ * Sets the item object of this FeedMedia. If the given
+ * FeedItem object is not null, it's 'media'-attribute value
+ * will also be set to this media object.
+ */
+ public void setItem(FeedItem item) {
+ this.item = item;
+ if (item != null && item.getMedia() != this) {
+ item.setMedia(this);
+ }
+ }
+
+ public Date getPlaybackCompletionDate() {
+ return playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ }
+
+ public void setPlaybackCompletionDate(Date playbackCompletionDate) {
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ }
+
+ public boolean isInProgress() {
+ return (this.position > 0);
+ }
+
+ public FeedImage getImage() {
+ if (item != null) {
+ return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage();
+ }
+ return null;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeLong(item.getId());
+
+ dest.writeInt(duration);
+ dest.writeInt(position);
+ dest.writeLong(size);
+ dest.writeString(mime_type);
+ dest.writeString(file_url);
+ dest.writeString(download_url);
+ dest.writeByte((byte) ((downloaded) ? 1 : 0));
+ dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0);
+ dest.writeInt(played_duration);
+ }
+
+ @Override
+ public void writeToPreferences(Editor prefEditor) {
+ prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId());
+ prefEditor.putLong(PREF_MEDIA_ID, id);
+ }
+
+ @Override
+ public void loadMetadata() throws PlayableException {
+ if (item == null && itemID != 0) {
+ item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID);
+ }
+ }
+
+ @Override
+ public void loadChapterMarks() {
+ if (getChapters() == null && !localFileAvailable()) {
+ ChapterUtils.loadChaptersFromStreamUrl(this);
+ if (getChapters() != null && item != null) {
+ DBWriter.setFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(),
+ item);
+ }
+ }
+
+ }
+
+ @Override
+ public String getEpisodeTitle() {
+ if (item == null) {
+ return null;
+ }
+ if (getItem().getTitle() != null) {
+ return getItem().getTitle();
+ } else {
+ return getItem().getIdentifyingValue();
+ }
+ }
+
+ @Override
+ public List<Chapter> getChapters() {
+ if (item == null) {
+ return null;
+ }
+ return getItem().getChapters();
+ }
+
+ @Override
+ public String getWebsiteLink() {
+ if (item == null) {
+ return null;
+ }
+ return getItem().getLink();
+ }
+
+ @Override
+ public String getFeedTitle() {
+ if (item == null) {
+ return null;
+ }
+ return getItem().getFeed().getTitle();
+ }
+
+ @Override
+ public Object getIdentifier() {
+ return id;
+ }
+
+ @Override
+ public String getLocalMediaUrl() {
+ return file_url;
+ }
+
+ @Override
+ public String getStreamUrl() {
+ return download_url;
+ }
+
+ @Override
+ public String getPaymentLink() {
+ if (item == null) {
+ return null;
+ }
+ return getItem().getPaymentLink();
+ }
+
+ @Override
+ public boolean localFileAvailable() {
+ return isDownloaded() && file_url != null;
+ }
+
+ @Override
+ public boolean streamAvailable() {
+ return download_url != null;
+ }
+
+ @Override
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
+ setPosition(newPosition);
+ DBWriter.setFeedMediaPlaybackInformation(ClientConfig.applicationCallbacks.getApplicationInstance(), this);
+ }
+
+ @Override
+ public void onPlaybackStart() {
+ }
+
+ @Override
+ public void onPlaybackCompleted() {
+
+ }
+
+ @Override
+ public int getPlayableType() {
+ return PLAYABLE_TYPE_FEEDMEDIA;
+ }
+
+ @Override
+ public void setChapters(List<Chapter> chapters) {
+ getItem().setChapters(chapters);
+ }
+
+ @Override
+ public Callable<String> loadShownotes() {
+ return new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ if (item == null) {
+ item = DBReader.getFeedItem(
+ ClientConfig.applicationCallbacks.getApplicationInstance(), itemID);
+ }
+ if (item.getContentEncoded() == null || item.getDescription() == null) {
+ DBReader.loadExtraInformationOfFeedItem(
+ ClientConfig.applicationCallbacks.getApplicationInstance(), item);
+
+ }
+ return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription();
+ }
+ };
+ }
+
+ public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() {
+ public FeedMedia createFromParcel(Parcel in) {
+ final long id = in.readLong();
+ final long itemID = in.readLong();
+ FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(),
+ in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt());
+ result.itemID = itemID;
+ return result;
+ }
+
+ public FeedMedia[] newArray(int size) {
+ return new FeedMedia[size];
+ }
+ };
+
+ @Override
+ public Uri getImageUri() {
+ final Uri feedImgUri = getFeedImageUri();
+
+ if (localFileAvailable()) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme(SCHEME_MEDIA)
+ .encodedPath(getLocalMediaUrl());
+ if (feedImgUri != null) {
+ builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString());
+ }
+ return builder.build();
+ } else if (item.hasItemImageDownloaded()) {
+ return item.getImage().getImageUri();
+ } else {
+ return feedImgUri;
+ }
+ }
+
+ private Uri getFeedImageUri() {
+ if (item != null && item.getFeed() != null) {
+ return item.getFeed().getImageUri();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java
new file mode 100644
index 000000000..2f0304182
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.content.Context;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Contains preferences for a single feed.
+ */
+public class FeedPreferences {
+
+ private long feedID;
+ private boolean autoDownload;
+ private String username;
+ private String password;
+
+ public FeedPreferences(long feedID, boolean autoDownload, String username, String password) {
+ this.feedID = feedID;
+ this.autoDownload = autoDownload;
+ this.username = username;
+ this.password = password;
+ }
+
+
+ /**
+ * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the
+ * comparison.
+ *
+ * @return True if the two objects are different.
+ */
+ public boolean compareWithOther(FeedPreferences other) {
+ if (other == null)
+ return true;
+ if (!StringUtils.equals(username, other.username)) {
+ return true;
+ }
+ if (!StringUtils.equals(password, other.password)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded
+ * from the update.
+ */
+ public void updateFromOther(FeedPreferences other) {
+ if (other == null)
+ return;
+ this.username = other.username;
+ this.password = other.password;
+ }
+
+ public long getFeedID() {
+ return feedID;
+ }
+
+ public void setFeedID(long feedID) {
+ this.feedID = feedID;
+ }
+
+ public boolean getAutoDownload() {
+ return autoDownload;
+ }
+
+ public void setAutoDownload(boolean autoDownload) {
+ this.autoDownload = autoDownload;
+ }
+
+ public void save(Context context) {
+ DBWriter.setFeedPreferences(context, this);
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java
new file mode 100644
index 000000000..f0ff03a93
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java
@@ -0,0 +1,36 @@
+package de.danoeh.antennapod.core.feed;
+
+public class ID3Chapter extends Chapter {
+ public static final int CHAPTERTYPE_ID3CHAPTER = 2;
+
+ /**
+ * Identifies the chapter in its ID3 tag. This attribute does not have to be
+ * store in the DB and is only used for parsing.
+ */
+ private String id3ID;
+
+ public ID3Chapter(String id3ID, long start) {
+ super(start);
+ this.id3ID = id3ID;
+ }
+
+ public ID3Chapter(long start, String title, FeedItem item, String link) {
+ super(start, title, item, link);
+ }
+
+ @Override
+ public String toString() {
+ return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start="
+ + start + ", url=" + link + "]";
+ }
+
+ @Override
+ public int getChapterType() {
+ return CHAPTERTYPE_ID3CHAPTER;
+ }
+
+ public String getId3ID() {
+ return id3ID;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java
new file mode 100644
index 000000000..7b3cb829d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java
@@ -0,0 +1,5 @@
+package de.danoeh.antennapod.core.feed;
+
+public enum MediaType {
+ AUDIO, VIDEO, UNKNOWN
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java
new file mode 100644
index 000000000..9aa8d3170
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.core.feed;
+
+public class SearchResult {
+ private FeedComponent component;
+ /** Additional information (e.g. where it was found) */
+ private String subtitle;
+ /** Higher value means more importance */
+ private int value;
+
+ public SearchResult(FeedComponent component, int value, String subtitle) {
+ super();
+ this.component = component;
+ this.value = value;
+ this.subtitle = subtitle;
+ }
+
+ public FeedComponent getComponent() {
+ return component;
+ }
+
+ public String getSubtitle() {
+ return subtitle;
+ }
+
+ public void setSubtitle(String subtitle) {
+ this.subtitle = subtitle;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java
new file mode 100644
index 000000000..2dadd3ec8
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.core.feed;
+
+public class SimpleChapter extends Chapter {
+ public static final int CHAPTERTYPE_SIMPLECHAPTER = 0;
+
+ public SimpleChapter(long start, String title, FeedItem item, String link) {
+ super(start, title, item, link);
+ }
+
+ @Override
+ public int getChapterType() {
+ return CHAPTERTYPE_SIMPLECHAPTER;
+ }
+
+ public void updateFromOther(SimpleChapter other) {
+ super.updateFromOther(other);
+ start = other.start;
+ if (other.title != null) {
+ title = other.title;
+ }
+ if (other.link != null) {
+ link = other.link;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java
new file mode 100644
index 000000000..5b54a2d59
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java
@@ -0,0 +1,109 @@
+package de.danoeh.antennapod.core.feed;
+
+import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException;
+
+import java.util.concurrent.TimeUnit;
+
+public class VorbisCommentChapter extends Chapter {
+ public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3;
+
+ private static final int CHAPTERXXX_LENGTH = "chapterxxx".length();
+
+ private int vorbisCommentId;
+
+ public VorbisCommentChapter(int vorbisCommentId) {
+ this.vorbisCommentId = vorbisCommentId;
+ }
+
+ public VorbisCommentChapter(long start, String title, FeedItem item,
+ String link) {
+ super(start, title, item, link);
+ }
+
+ @Override
+ public String toString() {
+ return "VorbisCommentChapter [id=" + id + ", title=" + title
+ + ", link=" + link + ", start=" + start + "]";
+ }
+
+ public static long getStartTimeFromValue(String value)
+ throws VorbisCommentReaderException {
+ String[] parts = value.split(":");
+ if (parts.length >= 3) {
+ try {
+ long hours = TimeUnit.MILLISECONDS.convert(
+ Long.parseLong(parts[0]), TimeUnit.HOURS);
+ long minutes = TimeUnit.MILLISECONDS.convert(
+ Long.parseLong(parts[1]), TimeUnit.MINUTES);
+ if (parts[2].contains("-->")) {
+ parts[2] = parts[2].substring(0, parts[2].indexOf("-->"));
+ }
+ long seconds = TimeUnit.MILLISECONDS.convert(
+ ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS);
+ return hours + minutes + seconds;
+ } catch (NumberFormatException e) {
+ throw new VorbisCommentReaderException(e);
+ }
+ } else {
+ throw new VorbisCommentReaderException("Invalid time string");
+ }
+ }
+
+ /**
+ * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx*
+ *
+ * @return the id of the chapter key or -1 if the id couldn't be read.
+ * @throws VorbisCommentReaderException
+ * */
+ public static int getIDFromKey(String key)
+ throws VorbisCommentReaderException {
+ if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx
+ try {
+ String strId = key.substring(8, 10);
+ return Integer.parseInt(strId);
+ } catch (NumberFormatException e) {
+ throw new VorbisCommentReaderException(e);
+ }
+ }
+ throw new VorbisCommentReaderException("key is too short (" + key + ")");
+ }
+
+ /**
+ * Get the string that comes after 'CHAPTERxxx', for example 'name' or
+ * 'url'.
+ */
+ public static String getAttributeTypeFromKey(String key) {
+ if (key.length() > CHAPTERXXX_LENGTH) {
+ return key.substring(CHAPTERXXX_LENGTH, key.length());
+ }
+ return null;
+ }
+
+ @Override
+ public int getChapterType() {
+ return CHAPTERTYPE_VORBISCOMMENT_CHAPTER;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public int getVorbisCommentId() {
+ return vorbisCommentId;
+ }
+
+ public void setVorbisCommentId(int vorbisCommentId) {
+ this.vorbisCommentId = vorbisCommentId;
+ }
+
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java
new file mode 100644
index 000000000..117cbf96b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java
@@ -0,0 +1,718 @@
+package de.danoeh.antennapod.core.gpoddernet;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.auth.AuthenticationException;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.auth.BasicScheme;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+
+/**
+ * Communicates with the gpodder.net service.
+ */
+public class GpodnetService {
+
+ private static final String BASE_SCHEME = "https";
+
+ public static final String DEFAULT_BASE_HOST = "gpodder.net";
+ private final String BASE_HOST;
+
+ private final HttpClient httpClient;
+
+ public GpodnetService() {
+ httpClient = AntennapodHttpClient.getHttpClient();
+ BASE_HOST = GpodnetPreferences.getHostname();
+ }
+
+ /**
+ * Returns the [count] most used tags.
+ */
+ public List<GpodnetTag> getTopTags(int count)
+ throws GpodnetServiceException {
+ URI uri;
+ try {
+ uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/tags/%d.json", count), null);
+ } catch (URISyntaxException e1) {
+ e1.printStackTrace();
+ throw new IllegalStateException(e1);
+ }
+
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ try {
+ JSONArray jsonTagList = new JSONArray(response);
+ List<GpodnetTag> tagList = new ArrayList<GpodnetTag>(
+ jsonTagList.length());
+ for (int i = 0; i < jsonTagList.length(); i++) {
+ JSONObject jObj = jsonTagList.getJSONObject(i);
+ String name = jObj.getString("tag");
+ int usage = jObj.getInt("usage");
+ tagList.add(new GpodnetTag(name, usage));
+ }
+ return tagList;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Returns the [count] most subscribed podcasts for the given tag.
+ *
+ * @throws IllegalArgumentException if tag is null
+ */
+ public List<GpodnetPodcast> getPodcastsForTag(GpodnetTag tag, int count)
+ throws GpodnetServiceException {
+ Validate.notNull(tag);
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/tag/%s/%d.json", tag.getName(), count), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+
+ }
+ }
+
+ /**
+ * Returns the toplist of podcast.
+ *
+ * @param count of elements that should be returned. Must be in range 1..100.
+ * @throws IllegalArgumentException if count is out of range.
+ */
+ public List<GpodnetPodcast> getPodcastToplist(int count)
+ throws GpodnetServiceException {
+ Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100");
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/toplist/%d.json", count), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+
+ }
+ }
+
+ /**
+ * Returns a list of suggested podcasts for the user that is currently
+ * logged in.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param count The
+ * number of elements that should be returned. Must be in range
+ * 1..100.
+ * @throws IllegalArgumentException if count is out of range.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public List<GpodnetPodcast> getSuggestions(int count) throws GpodnetServiceException {
+ Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100");
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/suggestions/%d.json", count), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Searches the podcast directory for a given string.
+ *
+ * @param query The search query
+ * @param scaledLogoSize The size of the logos that are returned by the search query.
+ * Must be in range 1..256. If the value is out of range, the
+ * default value defined by the gpodder.net API will be used.
+ */
+ public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize)
+ throws GpodnetServiceException {
+ String parameters = (scaledLogoSize > 0 && scaledLogoSize <= 256) ? String
+ .format("q=%s&scale_logo=%d", query, scaledLogoSize) : String
+ .format("q=%s", query);
+ try {
+ URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, "/search.json",
+ parameters, null);
+ System.out.println(uri.toASCIIString());
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+
+ JSONArray jsonArray = new JSONArray(response);
+ return readPodcastListFromJSONArray(jsonArray);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+
+ }
+ }
+
+ /**
+ * Returns all devices of a given user.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @throws IllegalArgumentException If username is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public List<GpodnetDevice> getDevices(String username)
+ throws GpodnetServiceException {
+ Validate.notNull(username);
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/devices/%s.json", username), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ JSONArray devicesArray = new JSONArray(response);
+ List<GpodnetDevice> result = readDeviceListFromJSONArray(devicesArray);
+
+ return result;
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Configures the device of a given user.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device that should be configured.
+ * @throws IllegalArgumentException If username or deviceId is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public void configureDevice(String username, String deviceId,
+ String caption, GpodnetDevice.DeviceType type)
+ throws GpodnetServiceException {
+ Validate.notNull(username);
+ Validate.notNull(deviceId);
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/devices/%s/%s.json", username, deviceId), null);
+ HttpPost request = new HttpPost(uri);
+ if (caption != null || type != null) {
+ JSONObject jsonContent = new JSONObject();
+ if (caption != null) {
+ jsonContent.put("caption", caption);
+ }
+ if (type != null) {
+ jsonContent.put("type", type.toString());
+ }
+ StringEntity strEntity = new StringEntity(
+ jsonContent.toString(), "UTF-8");
+ strEntity.setContentType("application/json");
+ request.setEntity(strEntity);
+ }
+ executeRequest(request);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalArgumentException(e);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
+ * Returns the subscriptions of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscriptions should be returned.
+ * @return A list of subscriptions in OPML format.
+ * @throws IllegalArgumentException If username or deviceId is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public String getSubscriptionsOfDevice(String username, String deviceId)
+ throws GpodnetServiceException {
+ Validate.notNull(username);
+ Validate.notNull(deviceId);
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/subscriptions/%s/%s.opml", username, deviceId), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ return response;
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Returns all subscriptions of a specific user.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @return A list of subscriptions in OPML format.
+ * @throws IllegalArgumentException If username is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public String getSubscriptionsOfUser(String username)
+ throws GpodnetServiceException {
+ Validate.notNull(username);
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/subscriptions/%s.opml", username), null);
+ HttpGet request = new HttpGet(uri);
+ String response = executeRequest(request);
+ return response;
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Uploads the subscriptions of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscriptions should be updated.
+ * @param subscriptions A list of feed URLs containing all subscriptions of the
+ * device.
+ * @throws IllegalArgumentException If username, deviceId or subscriptions is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public void uploadSubscriptions(String username, String deviceId,
+ List<String> subscriptions) throws GpodnetServiceException {
+ if (username == null || deviceId == null || subscriptions == null) {
+ throw new IllegalArgumentException(
+ "Username, device ID and subscriptions must not be null");
+ }
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/subscriptions/%s/%s.txt", username, deviceId), null);
+ HttpPut request = new HttpPut(uri);
+ StringBuilder builder = new StringBuilder();
+ for (String s : subscriptions) {
+ builder.append(s);
+ builder.append("\n");
+ }
+ StringEntity entity = new StringEntity(builder.toString(), "UTF-8");
+ request.setEntity(entity);
+
+ executeRequest(request);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Updates the subscription list of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscriptions should be updated.
+ * @param added Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates
+ * @param removed Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates
+ * @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse}
+ * for details.
+ * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null.
+ * @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there
+ * is an authentication error.
+ */
+ public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection<String> added,
+ Collection<String> removed) throws GpodnetServiceException {
+ Validate.notNull(username);
+ Validate.notNull(deviceId);
+ Validate.notNull(added);
+ Validate.notNull(removed);
+
+ try {
+ URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/subscriptions/%s/%s.json", username, deviceId), null);
+
+ final JSONObject requestObject = new JSONObject();
+ requestObject.put("add", new JSONArray(added));
+ requestObject.put("remove", new JSONArray(removed));
+
+ HttpPost request = new HttpPost(uri);
+ StringEntity entity = new StringEntity(requestObject.toString(), "UTF-8");
+ request.setEntity(entity);
+
+ final String response = executeRequest(request);
+ return GpodnetUploadChangesResponse.fromJSONObject(response);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ }
+
+ }
+
+ /**
+ * Returns all subscription changes of a specific device.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @param username The username. Must be the same user as the one which is
+ * currently logged in.
+ * @param deviceId The ID of the device whose subscription changes should be
+ * downloaded.
+ * @param timestamp A timestamp that can be used to receive all changes since a
+ * specific point in time.
+ * @throws IllegalArgumentException If username or deviceId is null.
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public GpodnetSubscriptionChange getSubscriptionChanges(String username,
+ String deviceId, long timestamp) throws GpodnetServiceException {
+ Validate.notNull(username);
+ Validate.notNull(deviceId);
+
+ String params = String.format("since=%d", timestamp);
+ String path = String.format("/api/2/subscriptions/%s/%s.json",
+ username, deviceId);
+ try {
+ URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params,
+ null);
+ HttpGet request = new HttpGet(uri);
+
+ String response = executeRequest(request);
+ JSONObject changes = new JSONObject(response);
+ return readSubscriptionChangesFromJSONObject(changes);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new IllegalStateException(e);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+
+ }
+
+ /**
+ * Logs in a specific user. This method must be called if any of the methods
+ * that require authentication is used.
+ *
+ * @throws IllegalArgumentException If username or password is null.
+ */
+ public void authenticate(String username, String password)
+ throws GpodnetServiceException {
+ Validate.notNull(username);
+ Validate.notNull(password);
+
+ URI uri;
+ try {
+ uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
+ "/api/2/auth/%s/login.json", username), null);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException();
+ }
+ HttpPost request = new HttpPost(uri);
+ executeRequestWithAuthentication(request, username, password);
+ }
+
+ /**
+ * Shuts down the GpodnetService's HTTP client. The service will be shut down in a separate thread to avoid
+ * NetworkOnMainThreadExceptions.
+ */
+ public void shutdown() {
+ new Thread() {
+ @Override
+ public void run() {
+ AntennapodHttpClient.cleanup();
+ }
+ }.start();
+ }
+
+ private String executeRequest(HttpRequestBase request)
+ throws GpodnetServiceException {
+ Validate.notNull(request);
+
+ String responseString = null;
+ HttpResponse response = null;
+ try {
+ response = httpClient.execute(request);
+ checkStatusCode(response);
+ responseString = getStringFromEntity(response.getEntity());
+ } catch (ClientProtocolException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } finally {
+ if (response != null) {
+ try {
+ response.getEntity().consumeContent();
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ }
+ return responseString;
+ }
+
+ private String executeRequestWithAuthentication(HttpRequestBase request,
+ String username, String password) throws GpodnetServiceException {
+ if (request == null || username == null || password == null) {
+ throw new IllegalArgumentException(
+ "request and credentials must not be null");
+ }
+ String result = null;
+ HttpResponse response = null;
+ try {
+ Header auth = new BasicScheme().authenticate(
+ new UsernamePasswordCredentials(username, password),
+ request);
+ request.addHeader(auth);
+ response = httpClient.execute(request);
+ checkStatusCode(response);
+ result = getStringFromEntity(response.getEntity());
+ } catch (ClientProtocolException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } catch (AuthenticationException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ } finally {
+ if (response != null) {
+ try {
+ response.getEntity().consumeContent();
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+ }
+ return result;
+ }
+
+ private String getStringFromEntity(HttpEntity entity)
+ throws GpodnetServiceException {
+ Validate.notNull(entity);
+
+ ByteArrayOutputStream outputStream;
+ int contentLength = (int) entity.getContentLength();
+ if (contentLength > 0) {
+ outputStream = new ByteArrayOutputStream(contentLength);
+ } else {
+ outputStream = new ByteArrayOutputStream();
+ }
+ try {
+ byte[] buffer = new byte[8 * 1024];
+ InputStream in = entity.getContent();
+ int count;
+ while ((count = in.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ // System.out.println(outputStream.toString());
+ return outputStream.toString();
+ }
+
+ private void checkStatusCode(HttpResponse response)
+ throws GpodnetServiceException {
+ Validate.notNull(response);
+ int responseCode = response.getStatusLine().getStatusCode();
+ if (responseCode != HttpStatus.SC_OK) {
+ if (responseCode == HttpStatus.SC_UNAUTHORIZED) {
+ throw new GpodnetServiceAuthenticationException("Wrong username or password");
+ } else {
+ throw new GpodnetServiceBadStatusCodeException(
+ "Bad response code: " + responseCode, responseCode);
+ }
+ }
+ }
+
+ private List<GpodnetPodcast> readPodcastListFromJSONArray(JSONArray array)
+ throws JSONException {
+ Validate.notNull(array);
+
+ List<GpodnetPodcast> result = new ArrayList<GpodnetPodcast>(
+ array.length());
+ for (int i = 0; i < array.length(); i++) {
+ result.add(readPodcastFromJSONObject(array.getJSONObject(i)));
+ }
+ return result;
+
+ }
+
+ private GpodnetPodcast readPodcastFromJSONObject(JSONObject object)
+ throws JSONException {
+ String url = object.getString("url");
+
+ String title;
+ Object titleObj = object.opt("title");
+ if (titleObj != null && titleObj instanceof String) {
+ title = (String) titleObj;
+ } else {
+ title = url;
+ }
+
+ String description;
+ Object descriptionObj = object.opt("description");
+ if (descriptionObj != null && descriptionObj instanceof String) {
+ description = (String) descriptionObj;
+ } else {
+ description = "";
+ }
+
+ int subscribers = object.getInt("subscribers");
+
+ Object logoUrlObj = object.opt("logo_url");
+ String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj
+ : null;
+ if (logoUrl == null) {
+ Object scaledLogoUrl = object.opt("scaled_logo_url");
+ if (scaledLogoUrl != null && scaledLogoUrl instanceof String) {
+ logoUrl = (String) scaledLogoUrl;
+ }
+ }
+
+ String website = null;
+ Object websiteObj = object.opt("website");
+ if (websiteObj != null && websiteObj instanceof String) {
+ website = (String) websiteObj;
+ }
+ String mygpoLink = object.getString("mygpo_link");
+ return new GpodnetPodcast(url, title, description, subscribers,
+ logoUrl, website, mygpoLink);
+ }
+
+ private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array)
+ throws JSONException {
+ Validate.notNull(array);
+
+ List<GpodnetDevice> result = new ArrayList<GpodnetDevice>(
+ array.length());
+ for (int i = 0; i < array.length(); i++) {
+ result.add(readDeviceFromJSONObject(array.getJSONObject(i)));
+ }
+ return result;
+ }
+
+ private GpodnetDevice readDeviceFromJSONObject(JSONObject object)
+ throws JSONException {
+ String id = object.getString("id");
+ String caption = object.getString("caption");
+ String type = object.getString("type");
+ int subscriptions = object.getInt("subscriptions");
+ return new GpodnetDevice(id, caption, type, subscriptions);
+ }
+
+ private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject(
+ JSONObject object) throws JSONException {
+ Validate.notNull(object);
+
+ List<String> added = new LinkedList<String>();
+ JSONArray jsonAdded = object.getJSONArray("add");
+ for (int i = 0; i < jsonAdded.length(); i++) {
+ added.add(jsonAdded.getString(i));
+ }
+
+ List<String> removed = new LinkedList<String>();
+ JSONArray jsonRemoved = object.getJSONArray("remove");
+ for (int i = 0; i < jsonRemoved.length(); i++) {
+ removed.add(jsonRemoved.getString(i));
+ }
+
+ long timestamp = object.getLong("timestamp");
+ return new GpodnetSubscriptionChange(added, removed, timestamp);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java
new file mode 100644
index 000000000..8bd56218c
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.core.gpoddernet;
+
+public class GpodnetServiceAuthenticationException extends GpodnetServiceException {
+
+ public GpodnetServiceAuthenticationException() {
+ super();
+ }
+
+ public GpodnetServiceAuthenticationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public GpodnetServiceAuthenticationException(String message) {
+ super(message);
+ }
+
+ public GpodnetServiceAuthenticationException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java
new file mode 100644
index 000000000..16f01f0f4
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java
@@ -0,0 +1,12 @@
+package de.danoeh.antennapod.core.gpoddernet;
+
+public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException {
+ int statusCode;
+
+ public GpodnetServiceBadStatusCodeException(String message, int statusCode) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java
new file mode 100644
index 000000000..ce704f7e3
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java
@@ -0,0 +1,19 @@
+package de.danoeh.antennapod.core.gpoddernet;
+
+public class GpodnetServiceException extends Exception {
+
+ public GpodnetServiceException() {
+ }
+
+ public GpodnetServiceException(String message) {
+ super(message);
+ }
+
+ public GpodnetServiceException(Throwable cause) {
+ super(cause);
+ }
+
+ public GpodnetServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java
new file mode 100644
index 000000000..4885a243a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java
@@ -0,0 +1,72 @@
+package de.danoeh.antennapod.core.gpoddernet.model;
+
+import org.apache.commons.lang3.Validate;
+
+public class GpodnetDevice {
+
+ private String id;
+ private String caption;
+ private DeviceType type;
+ private int subscriptions;
+
+ public GpodnetDevice(String id, String caption, String type,
+ int subscriptions) {
+ Validate.notNull(id);
+
+ this.id = id;
+ this.caption = caption;
+ this.type = DeviceType.fromString(type);
+ this.subscriptions = subscriptions;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type="
+ + type + ", subscriptions=" + subscriptions + "]";
+ }
+
+ public static enum DeviceType {
+ DESKTOP, LAPTOP, MOBILE, SERVER, OTHER;
+
+ static DeviceType fromString(String s) {
+ if (s == null) {
+ return OTHER;
+ }
+
+ if (s.equals("desktop")) {
+ return DESKTOP;
+ } else if (s.equals("laptop")) {
+ return LAPTOP;
+ } else if (s.equals("mobile")) {
+ return MOBILE;
+ } else if (s.equals("server")) {
+ return SERVER;
+ } else {
+ return OTHER;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+
+ public DeviceType getType() {
+ return type;
+ }
+
+ public int getSubscriptions() {
+ return subscriptions;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java
new file mode 100644
index 000000000..afebf66ac
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java
@@ -0,0 +1,65 @@
+package de.danoeh.antennapod.core.gpoddernet.model;
+
+import org.apache.commons.lang3.Validate;
+
+public class GpodnetPodcast {
+ private String url;
+ private String title;
+ private String description;
+ private int subscribers;
+ private String logoUrl;
+ private String website;
+ private String mygpoLink;
+
+ public GpodnetPodcast(String url, String title, String description,
+ int subscribers, String logoUrl, String website, String mygpoLink) {
+ Validate.notNull(url);
+ Validate.notNull(title);
+ Validate.notNull(description);
+
+ this.url = url;
+ this.title = title;
+ this.description = description;
+ this.subscribers = subscribers;
+ this.logoUrl = logoUrl;
+ this.website = website;
+ this.mygpoLink = mygpoLink;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetPodcast [url=" + url + ", title=" + title
+ + ", description=" + description + ", subscribers="
+ + subscribers + ", logoUrl=" + logoUrl + ", website=" + website
+ + ", mygpoLink=" + mygpoLink + "]";
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getSubscribers() {
+ return subscribers;
+ }
+
+ public String getLogoUrl() {
+ return logoUrl;
+ }
+
+ public String getWebsite() {
+ return website;
+ }
+
+ public String getMygpoLink() {
+ return mygpoLink;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java
new file mode 100644
index 000000000..a5cb8c0f0
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java
@@ -0,0 +1,41 @@
+package de.danoeh.antennapod.core.gpoddernet.model;
+
+import org.apache.commons.lang3.Validate;
+
+import java.util.List;
+
+public class GpodnetSubscriptionChange {
+ private List<String> added;
+ private List<String> removed;
+ private long timestamp;
+
+ public GpodnetSubscriptionChange(List<String> added, List<String> removed,
+ long timestamp) {
+ Validate.notNull(added);
+ Validate.notNull(removed);
+
+ this.added = added;
+ this.removed = removed;
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetSubscriptionChange [added=" + added.toString()
+ + ", removed=" + removed.toString() + ", timestamp="
+ + timestamp + "]";
+ }
+
+ public List<String> getAdded() {
+ return added;
+ }
+
+ public List<String> getRemoved() {
+ return removed;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java
new file mode 100644
index 000000000..7178f4be5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.core.gpoddernet.model;
+
+import org.apache.commons.lang3.Validate;
+
+import java.util.Comparator;
+
+public class GpodnetTag {
+
+ private String name;
+ private int usage;
+
+ public GpodnetTag(String name, int usage) {
+ Validate.notNull(name);
+
+ this.name = name;
+ this.usage = usage;
+ }
+
+ public GpodnetTag(String name) {
+ super();
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetTag [name=" + name + ", usage=" + usage + "]";
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getUsage() {
+ return usage;
+ }
+
+ public static class UsageComparator implements Comparator<GpodnetTag> {
+
+ @Override
+ public int compare(GpodnetTag o1, GpodnetTag o2) {
+ return o1.usage - o2.usage;
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java
new file mode 100644
index 000000000..5a37efa5e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java
@@ -0,0 +1,56 @@
+package de.danoeh.antennapod.core.gpoddernet.model;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Object returned by {@link de.danoeh.antennapod.core.gpoddernet.GpodnetService} in uploadChanges method.
+ */
+public class GpodnetUploadChangesResponse {
+
+ /**
+ * timestamp/ID that can be used for requesting changes since this upload.
+ */
+ public final long timestamp;
+
+ /**
+ * URLs that should be updated. The key of the map is the original URL, the value of the map
+ * is the sanitized URL.
+ */
+ public final Map<String, String> updatedUrls;
+
+ public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) {
+ this.timestamp = timestamp;
+ this.updatedUrls = updatedUrls;
+ }
+
+ /**
+ * Creates a new GpodnetUploadChangesResponse-object from a JSON object that was
+ * returned by an uploadChanges call.
+ *
+ * @throws org.json.JSONException If the method could not parse the JSONObject.
+ */
+ public static GpodnetUploadChangesResponse fromJSONObject(String objectString) throws JSONException {
+ final JSONObject object = new JSONObject(objectString);
+ final long timestamp = object.getLong("timestamp");
+ Map<String, String> updatedUrls = new HashMap<String, String>();
+ JSONArray urls = object.getJSONArray("update_urls");
+ for (int i = 0; i < urls.length(); i++) {
+ JSONArray urlPair = urls.getJSONArray(i);
+ updatedUrls.put(urlPair.getString(0), urlPair.getString(1));
+ }
+ return new GpodnetUploadChangesResponse(timestamp, updatedUrls);
+ }
+
+ @Override
+ public String toString() {
+ return "GpodnetUploadChangesResponse{" +
+ "timestamp=" + timestamp +
+ ", updatedUrls=" + updatedUrls +
+ '}';
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java
new file mode 100644
index 000000000..8d0a4a842
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.core.opml;
+
+/** Represents a single feed in an OPML file. */
+public class OpmlElement {
+ private String text;
+ private String xmlUrl;
+ private String htmlUrl;
+ private String type;
+
+ public OpmlElement() {
+
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public String getXmlUrl() {
+ return xmlUrl;
+ }
+
+ public void setXmlUrl(String xmlUrl) {
+ this.xmlUrl = xmlUrl;
+ }
+
+ public String getHtmlUrl() {
+ return htmlUrl;
+ }
+
+ public void setHtmlUrl(String htmlUrl) {
+ this.htmlUrl = htmlUrl;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java
new file mode 100644
index 000000000..775129d09
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java
@@ -0,0 +1,87 @@
+package de.danoeh.antennapod.core.opml;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+
+/** Reads OPML documents. */
+public class OpmlReader {
+ private static final String TAG = "OpmlReader";
+
+ // ATTRIBUTES
+ private boolean isInOpml = false;
+ private ArrayList<OpmlElement> elementList;
+
+ /**
+ * Reads an Opml document and returns a list of all OPML elements it can
+ * find
+ *
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ public ArrayList<OpmlElement> readDocument(Reader reader)
+ throws XmlPullParserException, IOException {
+ elementList = new ArrayList<OpmlElement>();
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ XmlPullParser xpp = factory.newPullParser();
+ xpp.setInput(reader);
+ int eventType = xpp.getEventType();
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ switch (eventType) {
+ case XmlPullParser.START_DOCUMENT:
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Reached beginning of document");
+ break;
+ case XmlPullParser.START_TAG:
+ if (xpp.getName().equals(OpmlSymbols.OPML)) {
+ isInOpml = true;
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Reached beginning of OPML tree.");
+ } else if (isInOpml && xpp.getName().equals(OpmlSymbols.OUTLINE)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Found new Opml element");
+ OpmlElement element = new OpmlElement();
+
+ final String title = xpp.getAttributeValue(null, OpmlSymbols.TITLE);
+ if (title != null) {
+ Log.i(TAG, "Using title: " + title);
+ element.setText(title);
+ } else {
+ Log.i(TAG, "Title not found, using text");
+ element.setText(xpp.getAttributeValue(null, OpmlSymbols.TEXT));
+ }
+ element.setXmlUrl(xpp.getAttributeValue(null, OpmlSymbols.XMLURL));
+ element.setHtmlUrl(xpp.getAttributeValue(null, OpmlSymbols.HTMLURL));
+ element.setType(xpp.getAttributeValue(null, OpmlSymbols.TYPE));
+ if (element.getXmlUrl() != null) {
+ if (element.getText() == null) {
+ Log.i(TAG, "Opml element has no text attribute.");
+ element.setText(element.getXmlUrl());
+ }
+ elementList.add(element);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Skipping element because of missing xml url");
+ }
+ }
+ break;
+ }
+ eventType = xpp.next();
+ }
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Parsing finished.");
+
+ return elementList;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java
new file mode 100644
index 000000000..2b831ca2a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.core.opml;
+
+/** Contains symbols for reading and writing OPML documents. */
+public final class OpmlSymbols {
+
+ public static final String OPML = "opml";
+ public static final String BODY = "body";
+ public static final String OUTLINE = "outline";
+ public static final String TEXT = "text";
+ public static final String XMLURL = "xmlUrl";
+ public static final String HTMLURL = "htmlUrl";
+ public static final String TYPE = "type";
+ public static final String VERSION = "version";
+ public static final String HEAD = "head";
+ public static final String TITLE = "title";
+
+ private OpmlSymbols() {
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java
new file mode 100644
index 000000000..641190f62
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java
@@ -0,0 +1,65 @@
+package de.danoeh.antennapod.core.opml;
+
+import android.util.Log;
+import android.util.Xml;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Feed;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+
+/** Writes OPML documents. */
+public class OpmlWriter {
+ private static final String TAG = "OpmlWriter";
+ private static final String ENCODING = "UTF-8";
+ private static final String OPML_VERSION = "2.0";
+ private static final String OPML_TITLE = "AntennaPod Subscriptions";
+
+ /**
+ * Takes a list of feeds and a writer and writes those into an OPML
+ * document.
+ *
+ * @throws IOException
+ * @throws IllegalStateException
+ * @throws IllegalArgumentException
+ */
+ public void writeDocument(List<Feed> feeds, Writer writer)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting to write document");
+ XmlSerializer xs = Xml.newSerializer();
+ xs.setOutput(writer);
+
+ xs.startDocument(ENCODING, false);
+ xs.startTag(null, OpmlSymbols.OPML);
+ xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION);
+
+ xs.startTag(null, OpmlSymbols.HEAD);
+ xs.startTag(null, OpmlSymbols.TITLE);
+ xs.text(OPML_TITLE);
+ xs.endTag(null, OpmlSymbols.TITLE);
+ xs.endTag(null, OpmlSymbols.HEAD);
+
+ xs.startTag(null, OpmlSymbols.BODY);
+ for (Feed feed : feeds) {
+ xs.startTag(null, OpmlSymbols.OUTLINE);
+ xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle());
+ xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle());
+ if (feed.getType() != null) {
+ xs.attribute(null, OpmlSymbols.TYPE, feed.getType());
+ }
+ xs.attribute(null, OpmlSymbols.XMLURL, feed.getDownload_url());
+ if (feed.getLink() != null) {
+ xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink());
+ }
+ xs.endTag(null, OpmlSymbols.OUTLINE);
+ }
+ xs.endTag(null, OpmlSymbols.BODY);
+ xs.endTag(null, OpmlSymbols.OPML);
+ xs.endDocument();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Finished writing document");
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java
new file mode 100644
index 000000000..af04df017
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java
@@ -0,0 +1,247 @@
+package de.danoeh.antennapod.core.preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.service.GpodnetSyncService;
+
+/**
+ * Manages preferences for accessing gpodder.net service
+ */
+public class GpodnetPreferences {
+
+ private static final String TAG = "GpodnetPreferences";
+
+ private static final String PREF_NAME = "gpodder.net";
+ public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username";
+ public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password";
+ public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
+ public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname";
+
+
+ public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
+ public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added";
+ public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed";
+
+ private static String username;
+ private static String password;
+ private static String deviceID;
+ private static String hostname;
+
+ private static ReentrantLock feedListLock = new ReentrantLock();
+ private static Set<String> addedFeeds;
+ private static Set<String> removedFeeds;
+
+ /**
+ * Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
+ */
+ private static long lastSyncTimestamp;
+
+ private static boolean preferencesLoaded = false;
+
+ private static SharedPreferences getPreferences() {
+ return ClientConfig.applicationCallbacks.getApplicationInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ }
+
+ private static synchronized void ensurePreferencesLoaded() {
+ if (!preferencesLoaded) {
+ SharedPreferences prefs = getPreferences();
+ username = prefs.getString(PREF_GPODNET_USERNAME, null);
+ password = prefs.getString(PREF_GPODNET_PASSWORD, null);
+ deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
+ lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0);
+ addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, ""));
+ removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, ""));
+ hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST));
+
+ preferencesLoaded = true;
+ }
+ }
+
+ private static void writePreference(String key, String value) {
+ SharedPreferences.Editor editor = getPreferences().edit();
+ editor.putString(key, value);
+ editor.commit();
+ }
+
+ private static void writePreference(String key, long value) {
+ SharedPreferences.Editor editor = getPreferences().edit();
+ editor.putLong(key, value);
+ editor.commit();
+ }
+
+ private static void writePreference(String key, Collection<String> value) {
+ SharedPreferences.Editor editor = getPreferences().edit();
+ editor.putString(key, writeListToString(value));
+ editor.commit();
+ }
+
+ public static String getUsername() {
+ ensurePreferencesLoaded();
+ return username;
+ }
+
+ public static void setUsername(String username) {
+ GpodnetPreferences.username = username;
+ writePreference(PREF_GPODNET_USERNAME, username);
+ }
+
+ public static String getPassword() {
+ ensurePreferencesLoaded();
+ return password;
+ }
+
+ public static void setPassword(String password) {
+ GpodnetPreferences.password = password;
+ writePreference(PREF_GPODNET_PASSWORD, password);
+ }
+
+ public static String getDeviceID() {
+ ensurePreferencesLoaded();
+ return deviceID;
+ }
+
+ public static void setDeviceID(String deviceID) {
+ GpodnetPreferences.deviceID = deviceID;
+ writePreference(PREF_GPODNET_DEVICEID, deviceID);
+ }
+
+ public static long getLastSyncTimestamp() {
+ ensurePreferencesLoaded();
+ return lastSyncTimestamp;
+ }
+
+ public static void setLastSyncTimestamp(long lastSyncTimestamp) {
+ GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp;
+ writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp);
+ }
+
+ public static String getHostname() {
+ ensurePreferencesLoaded();
+ return hostname;
+ }
+
+ public static void setHostname(String value) {
+ value = checkGpodnetHostname(value);
+ if (!value.equals(hostname)) {
+ logout();
+ writePreference(PREF_GPODNET_HOSTNAME, value);
+ hostname = value;
+ }
+ }
+
+ public static void addAddedFeed(String feed) {
+ ensurePreferencesLoaded();
+ feedListLock.lock();
+ if (addedFeeds.add(feed)) {
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ }
+ if (removedFeeds.remove(feed)) {
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+ }
+ feedListLock.unlock();
+ GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance());
+ }
+
+ public static void addRemovedFeed(String feed) {
+ ensurePreferencesLoaded();
+ feedListLock.lock();
+ if (removedFeeds.add(feed)) {
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+ }
+ if (addedFeeds.remove(feed)) {
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ }
+ feedListLock.unlock();
+ GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance());
+ }
+
+ public static Set<String> getAddedFeedsCopy() {
+ ensurePreferencesLoaded();
+ Set<String> copy = new HashSet<String>();
+ feedListLock.lock();
+ copy.addAll(addedFeeds);
+ feedListLock.unlock();
+ return copy;
+ }
+
+ public static void removeAddedFeeds(Collection<String> removed) {
+ ensurePreferencesLoaded();
+ feedListLock.lock();
+ addedFeeds.removeAll(removed);
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ feedListLock.unlock();
+ }
+
+ public static Set<String> getRemovedFeedsCopy() {
+ ensurePreferencesLoaded();
+ Set<String> copy = new HashSet<String>();
+ feedListLock.lock();
+ copy.addAll(removedFeeds);
+ feedListLock.unlock();
+ return copy;
+ }
+
+ public static void removeRemovedFeeds(Collection<String> removed) {
+ ensurePreferencesLoaded();
+ removedFeeds.removeAll(removed);
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+
+ }
+
+ /**
+ * Returns true if device ID, username and password have a non-null value
+ */
+ public static boolean loggedIn() {
+ ensurePreferencesLoaded();
+ return deviceID != null && username != null && password != null;
+ }
+
+ public static synchronized void logout() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences");
+ setUsername(null);
+ setPassword(null);
+ setDeviceID(null);
+ addedFeeds.clear();
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ removedFeeds.clear();
+ writePreference(PREF_SYNC_REMOVED, removedFeeds);
+ setLastSyncTimestamp(0);
+ }
+
+ private static Set<String> readListFromString(String s) {
+ Set<String> result = new HashSet<String>();
+ for (String item : s.split(" ")) {
+ result.add(item);
+ }
+ return result;
+ }
+
+ private static String writeListToString(Collection<String> c) {
+ StringBuilder result = new StringBuilder();
+ for (String item : c) {
+ result.append(item);
+ result.append(" ");
+ }
+ return result.toString().trim();
+ }
+
+ private static String checkGpodnetHostname(String value) {
+ int startIndex = 0;
+ if (value.startsWith("http://")) {
+ startIndex = "http://".length();
+ } else if (value.startsWith("https://")) {
+ startIndex = "https://".length();
+ }
+ return value.substring(startIndex);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
new file mode 100644
index 000000000..d88543f73
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
@@ -0,0 +1,146 @@
+package de.danoeh.antennapod.core.preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.core.BuildConfig;
+
+/**
+ * Provides access to preferences set by the playback service. A private
+ * instance of this class must first be instantiated via createInstance() or
+ * otherwise every public method will throw an Exception when called.
+ */
+public class PlaybackPreferences implements
+ SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = "PlaybackPreferences";
+
+ /**
+ * Contains the feed id of the currently playing item if it is a FeedMedia
+ * object.
+ */
+ public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId";
+
+ /**
+ * Contains the id of the currently playing FeedMedia object or
+ * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object.
+ */
+ public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId";
+
+ /**
+ * Type of the media object that is currently being played. This preference
+ * is set to NO_MEDIA_PLAYING after playback has been completed and is set
+ * as soon as the 'play' button is pressed.
+ */
+ public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia";
+
+ /** True if last played media was streamed. */
+ public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
+
+ /** True if last played media was a video. */
+ public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo";
+
+ /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */
+ public static final long NO_MEDIA_PLAYING = -1;
+
+ private long currentlyPlayingFeedId;
+ private long currentlyPlayingFeedMediaId;
+ private long currentlyPlayingMedia;
+ private boolean currentEpisodeIsStream;
+ private boolean currentEpisodeIsVideo;
+
+ private static PlaybackPreferences instance;
+ private Context context;
+
+ private PlaybackPreferences(Context context) {
+ this.context = context;
+ loadPreferences();
+ }
+
+ /**
+ * Sets up the UserPreferences class.
+ *
+ * @throws IllegalArgumentException
+ * if context is null
+ * */
+ public static void createInstance(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Creating new instance of UserPreferences");
+ Validate.notNull(context);
+
+ instance = new PlaybackPreferences(context);
+
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .registerOnSharedPreferenceChangeListener(instance);
+ }
+
+ private void loadPreferences() {
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1);
+ currentlyPlayingFeedMediaId = sp.getLong(
+ PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
+ currentlyPlayingMedia = sp.getLong(PREF_CURRENTLY_PLAYING_MEDIA,
+ NO_MEDIA_PLAYING);
+ currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
+ currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
+ if (key.equals(PREF_CURRENTLY_PLAYING_FEED_ID)) {
+ currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID,
+ -1);
+
+ } else if (key.equals(PREF_CURRENTLY_PLAYING_MEDIA)) {
+ currentlyPlayingMedia = sp
+ .getLong(PREF_CURRENTLY_PLAYING_MEDIA, -1);
+
+ } else if (key.equals(PREF_CURRENT_EPISODE_IS_STREAM)) {
+ currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
+
+ } else if (key.equals(PREF_CURRENT_EPISODE_IS_VIDEO)) {
+ currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
+
+ } else if (key.equals(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID)) {
+ currentlyPlayingFeedMediaId = sp.getLong(
+ PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
+ }
+ }
+
+ private static void instanceAvailable() {
+ if (instance == null) {
+ throw new IllegalStateException(
+ "UserPreferences was used before being set up");
+ }
+ }
+
+
+ public static long getLastPlayedFeedId() {
+ instanceAvailable();
+ return instance.currentlyPlayingFeedId;
+ }
+
+ public static long getCurrentlyPlayingMedia() {
+ instanceAvailable();
+ return instance.currentlyPlayingMedia;
+ }
+
+ public static long getCurrentlyPlayingFeedMediaId() {
+ return instance.currentlyPlayingFeedMediaId;
+ }
+
+ public static boolean getCurrentEpisodeIsStream() {
+ instanceAvailable();
+ return instance.currentEpisodeIsStream;
+ }
+
+ public static boolean getCurrentEpisodeIsVideo() {
+ instanceAvailable();
+ return instance.currentEpisodeIsVideo;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
new file mode 100644
index 000000000..f4d44c4da
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
@@ -0,0 +1,607 @@
+package de.danoeh.antennapod.core.preferences;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver;
+
+/**
+ * Provides access to preferences set by the user in the settings screen. A
+ * private instance of this class must first be instantiated via
+ * createInstance() or otherwise every public method will throw an Exception
+ * when called.
+ */
+public class UserPreferences implements
+ SharedPreferences.OnSharedPreferenceChangeListener {
+ public static final String IMPORT_DIR = "import/";
+ private static final String TAG = "UserPreferences";
+
+ public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect";
+ public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue";
+ public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly";
+ public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall";
+ public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate";
+ public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes";
+ public static final String PREF_AUTO_DELETE = "prefAutoDelete";
+ public static final String PREF_AUTO_FLATTR = "pref_auto_flattr";
+ public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold";
+ public static final String PREF_THEME = "prefTheme";
+ public static final String PREF_DATA_FOLDER = "prefDataFolder";
+ public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl";
+ public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter";
+ private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks";
+ public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize";
+ private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed";
+ private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
+ public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss";
+ private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs";
+ private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
+ private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify";
+
+ // TODO: Make this value configurable
+ private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f;
+
+ private static int EPISODE_CACHE_SIZE_UNLIMITED = -1;
+
+ private static UserPreferences instance;
+ private final Context context;
+
+ // Preferences
+ private boolean pauseOnHeadsetDisconnect;
+ private boolean followQueue;
+ private boolean downloadMediaOnWifiOnly;
+ private long updateInterval;
+ private boolean allowMobileUpdate;
+ private boolean displayOnlyEpisodes;
+ private boolean autoDelete;
+ private boolean autoFlattr;
+ private float autoFlattrPlayedDurationThreshold;
+ private int theme;
+ private boolean enableAutodownload;
+ private boolean enableAutodownloadWifiFilter;
+ private String[] autodownloadSelectedNetworks;
+ private int episodeCacheSize;
+ private String playbackSpeed;
+ private String[] playbackSpeedArray;
+ private boolean pauseForFocusLoss;
+ private int seekDeltaSecs;
+ private boolean isFreshInstall;
+ private int notifyPriority;
+ private boolean persistNotify;
+
+ private UserPreferences(Context context) {
+ this.context = context;
+ loadPreferences();
+ }
+
+ /**
+ * Sets up the UserPreferences class.
+ *
+ * @throws IllegalArgumentException if context is null
+ */
+ public static void createInstance(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Creating new instance of UserPreferences");
+ Validate.notNull(context);
+
+ instance = new UserPreferences(context);
+
+ createImportDirectory();
+ createNoMediaFile();
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .registerOnSharedPreferenceChangeListener(instance);
+
+ }
+
+ private void loadPreferences() {
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger(
+ R.integer.episode_cache_size_unlimited);
+ pauseOnHeadsetDisconnect = sp.getBoolean(
+ PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
+ followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false);
+ downloadMediaOnWifiOnly = sp.getBoolean(
+ PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true);
+ updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL,
+ "0"));
+ allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false);
+ displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false);
+ autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false);
+ autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
+ autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
+ PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
+ theme = readThemeValue(sp.getString(PREF_THEME, "0"));
+ enableAutodownloadWifiFilter = sp.getBoolean(
+ PREF_ENABLE_AUTODL_WIFI_FILTER, false);
+ autodownloadSelectedNetworks = StringUtils.split(
+ sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ',');
+ episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(
+ PREF_EPISODE_CACHE_SIZE, "20"));
+ enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
+ playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
+ playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
+ PREF_PLAYBACK_SPEED_ARRAY, null));
+ pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
+ seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30"));
+ if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
+ notifyPriority = NotificationCompat.PRIORITY_MAX;
+ }
+ else {
+ notifyPriority = NotificationCompat.PRIORITY_DEFAULT;
+ }
+ persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false);
+ }
+
+ private int readThemeValue(String valueFromPrefs) {
+ switch (Integer.parseInt(valueFromPrefs)) {
+ case 0:
+ return R.style.Theme_AntennaPod_Light;
+ case 1:
+ return R.style.Theme_AntennaPod_Dark;
+ default:
+ return R.style.Theme_AntennaPod_Light;
+ }
+ }
+
+ private long readUpdateInterval(String valueFromPrefs) {
+ int hours = Integer.parseInt(valueFromPrefs);
+ return TimeUnit.HOURS.toMillis(hours);
+ }
+
+ private int readEpisodeCacheSizeInternal(String valueFromPrefs) {
+ if (valueFromPrefs.equals(context
+ .getString(R.string.pref_episode_cache_unlimited))) {
+ return EPISODE_CACHE_SIZE_UNLIMITED;
+ } else {
+ return Integer.valueOf(valueFromPrefs);
+ }
+ }
+
+ private String[] readPlaybackSpeedArray(String valueFromPrefs) {
+ String[] selectedSpeeds = null;
+ // If this preference hasn't been set yet, return the default options
+ if (valueFromPrefs == null) {
+ String[] allSpeeds = context.getResources().getStringArray(
+ R.array.playback_speed_values);
+ List<String> speedList = new LinkedList<String>();
+ for (String speedStr : allSpeeds) {
+ float speed = Float.parseFloat(speedStr);
+ if (speed < 2.0001 && speed * 10 % 1 == 0) {
+ speedList.add(speedStr);
+ }
+ }
+ selectedSpeeds = speedList.toArray(new String[speedList.size()]);
+ } else {
+ try {
+ JSONArray jsonArray = new JSONArray(valueFromPrefs);
+ selectedSpeeds = new String[jsonArray.length()];
+ for (int i = 0; i < jsonArray.length(); i++) {
+ selectedSpeeds[i] = jsonArray.getString(i);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG,
+ "Got JSON error when trying to get speeds from JSONArray");
+ e.printStackTrace();
+ }
+ }
+ return selectedSpeeds;
+ }
+
+ private static void instanceAvailable() {
+ if (instance == null) {
+ throw new IllegalStateException(
+ "UserPreferences was used before being set up");
+ }
+ }
+
+ public static boolean isPauseOnHeadsetDisconnect() {
+ instanceAvailable();
+ return instance.pauseOnHeadsetDisconnect;
+ }
+
+ public static boolean isFollowQueue() {
+ instanceAvailable();
+ return instance.followQueue;
+ }
+
+ public static boolean isDownloadMediaOnWifiOnly() {
+ instanceAvailable();
+ return instance.downloadMediaOnWifiOnly;
+ }
+
+ public static long getUpdateInterval() {
+ instanceAvailable();
+ return instance.updateInterval;
+ }
+
+ public static boolean isAllowMobileUpdate() {
+ instanceAvailable();
+ return instance.allowMobileUpdate;
+ }
+
+ public static boolean isDisplayOnlyEpisodes() {
+ instanceAvailable();
+ //return instance.displayOnlyEpisodes;
+ return false;
+ }
+
+ public static boolean isAutoDelete() {
+ instanceAvailable();
+ return instance.autoDelete;
+ }
+
+ public static boolean isAutoFlattr() {
+ instanceAvailable();
+ return instance.autoFlattr;
+ }
+
+ public static int getNotifyPriority() {
+ instanceAvailable();
+ return instance.notifyPriority;
+ }
+
+ public static boolean isPersistNotify() {
+ instanceAvailable();
+ return instance.persistNotify;
+ }
+
+
+ /**
+ * Returns the time after which an episode should be auto-flattr'd in percent of the episode's
+ * duration.
+ */
+ public static float getAutoFlattrPlayedDurationThreshold() {
+ instanceAvailable();
+ return instance.autoFlattrPlayedDurationThreshold;
+ }
+
+ public static int getTheme() {
+ instanceAvailable();
+ return instance.theme;
+ }
+
+ public static boolean isEnableAutodownloadWifiFilter() {
+ instanceAvailable();
+ return instance.enableAutodownloadWifiFilter;
+ }
+
+ public static String[] getAutodownloadSelectedNetworks() {
+ instanceAvailable();
+ return instance.autodownloadSelectedNetworks;
+ }
+
+ public static int getEpisodeCacheSizeUnlimited() {
+ return EPISODE_CACHE_SIZE_UNLIMITED;
+ }
+
+ public static String getPlaybackSpeed() {
+ instanceAvailable();
+ return instance.playbackSpeed;
+ }
+
+ public static String[] getPlaybackSpeedArray() {
+ instanceAvailable();
+ return instance.playbackSpeedArray;
+ }
+
+ public static int getSeekDeltaMs() {
+ instanceAvailable();
+ return 1000 * instance.seekDeltaSecs;
+ }
+
+ /**
+ * Returns the capacity of the episode cache. This method will return the
+ * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
+ * 'unlimited'.
+ */
+ public static int getEpisodeCacheSize() {
+ instanceAvailable();
+ return instance.episodeCacheSize;
+ }
+
+ public static boolean isEnableAutodownload() {
+ instanceAvailable();
+ return instance.enableAutodownload;
+ }
+
+ public static boolean shouldPauseForFocusLoss() {
+ instanceAvailable();
+ return instance.pauseForFocusLoss;
+ }
+
+ public static boolean isFreshInstall() {
+ instanceAvailable();
+ return instance.isFreshInstall;
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Registered change of user preferences. Key: " + key);
+
+ if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) {
+ downloadMediaOnWifiOnly = sp.getBoolean(
+ PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true);
+
+ } else if (key.equals(PREF_MOBILE_UPDATE)) {
+ allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false);
+
+ } else if (key.equals(PREF_FOLLOW_QUEUE)) {
+ followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false);
+
+ } else if (key.equals(PREF_UPDATE_INTERVAL)) {
+ updateInterval = readUpdateInterval(sp.getString(
+ PREF_UPDATE_INTERVAL, "0"));
+ ClientConfig.applicationCallbacks.setUpdateInterval(updateInterval);
+
+ } else if (key.equals(PREF_AUTO_DELETE)) {
+ autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false);
+
+ } else if (key.equals(PREF_AUTO_FLATTR)) {
+ autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
+ } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) {
+ displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES,
+ false);
+ } else if (key.equals(PREF_THEME)) {
+ theme = readThemeValue(sp.getString(PREF_THEME, ""));
+ } else if (key.equals(PREF_ENABLE_AUTODL_WIFI_FILTER)) {
+ enableAutodownloadWifiFilter = sp.getBoolean(
+ PREF_ENABLE_AUTODL_WIFI_FILTER, false);
+ } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) {
+ autodownloadSelectedNetworks = StringUtils.split(
+ sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ',');
+ } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) {
+ episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(
+ PREF_EPISODE_CACHE_SIZE, "20"));
+ } else if (key.equals(PREF_ENABLE_AUTODL)) {
+ enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
+ } else if (key.equals(PREF_PLAYBACK_SPEED)) {
+ playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
+ } else if (key.equals(PREF_PLAYBACK_SPEED_ARRAY)) {
+ playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
+ PREF_PLAYBACK_SPEED_ARRAY, null));
+ } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) {
+ pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
+ } else if (key.equals(PREF_SEEK_DELTA_SECS)) {
+ seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30"));
+ } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) {
+ pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
+ } else if (key.equals(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD)) {
+ autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
+ PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
+ } else if (key.equals(PREF_EXPANDED_NOTIFICATION)) {
+ if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
+ notifyPriority = NotificationCompat.PRIORITY_MAX;
+ }
+ else {
+ notifyPriority = NotificationCompat.PRIORITY_DEFAULT;
+ }
+ } else if (key.equals(PREF_PERSISTENT_NOTIFICATION)) {
+ persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false);
+ }
+ }
+
+ public static void setPlaybackSpeed(String speed) {
+ PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
+ .putString(PREF_PLAYBACK_SPEED, speed).apply();
+ }
+
+ public static void setPlaybackSpeedArray(String[] speeds) {
+ JSONArray jsonArray = new JSONArray();
+ for (String speed : speeds) {
+ jsonArray.put(speed);
+ }
+ PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
+ .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString())
+ .apply();
+ }
+
+ public static void setAutodownloadSelectedNetworks(Context context,
+ String[] value) {
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(context.getApplicationContext())
+ .edit();
+ editor.putString(PREF_AUTODL_SELECTED_NETWORKS,
+ StringUtils.join(value, ','));
+ editor.commit();
+ }
+
+ /**
+ * Sets the update interval value. Should only be used for testing purposes!
+ */
+ public static void setUpdateInterval(Context context, long newValue) {
+ instanceAvailable();
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(context.getApplicationContext())
+ .edit();
+ editor.putString(PREF_UPDATE_INTERVAL,
+ String.valueOf(newValue));
+ editor.commit();
+ instance.updateInterval = newValue;
+ }
+
+ /**
+ * Change the auto-flattr settings
+ *
+ * @param context For accessing the shared preferences
+ * @param enabled Whether automatic flattring should be enabled at all
+ * @param autoFlattrThreshold The percentage of playback time after which an episode should be
+ * flattrd. Must be a value between 0 and 1 (inclusive)
+ * */
+ public static void setAutoFlattrSettings(Context context, boolean enabled, float autoFlattrThreshold) {
+ instanceAvailable();
+ Validate.inclusiveBetween(0.0, 1.0, autoFlattrThreshold);
+ PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext())
+ .edit()
+ .putBoolean(PREF_AUTO_FLATTR, enabled)
+ .putFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, autoFlattrThreshold)
+ .commit();
+ instance.autoFlattr = enabled;
+ instance.autoFlattrPlayedDurationThreshold = autoFlattrThreshold;
+ }
+
+ /**
+ * Return the folder where the app stores all of its data. This method will
+ * return the standard data folder if none has been set by the user.
+ *
+ * @param type The name of the folder inside the data folder. May be null
+ * when accessing the root of the data folder.
+ * @return The data folder that has been requested or null if the folder
+ * could not be created.
+ */
+ public static File getDataFolder(Context context, String type) {
+ instanceAvailable();
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(context.getApplicationContext());
+ String strDir = prefs.getString(PREF_DATA_FOLDER, null);
+ if (strDir == null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Using default data folder");
+ return context.getExternalFilesDir(type);
+ } else {
+ File dataDir = new File(strDir);
+ if (!dataDir.exists()) {
+ if (!dataDir.mkdir()) {
+ Log.w(TAG, "Could not create data folder");
+ return null;
+ }
+ }
+
+ if (type == null) {
+ return dataDir;
+ } else {
+ // handle path separators
+ String[] dirs = type.split("/");
+ for (int i = 0; i < dirs.length; i++) {
+ if (dirs.length > 0) {
+ if (i < dirs.length - 1) {
+ dataDir = getDataFolder(context, dirs[i]);
+ if (dataDir == null) {
+ return null;
+ }
+ }
+ type = dirs[i];
+ }
+ }
+ File typeDir = new File(dataDir, type);
+ if (!typeDir.exists()) {
+ if (dataDir.canWrite()) {
+ if (!typeDir.mkdir()) {
+ Log.e(TAG, "Could not create data folder named "
+ + type);
+ return null;
+ }
+ }
+ }
+ return typeDir;
+ }
+
+ }
+ }
+
+ public static void setDataFolder(String dir) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Result from DirectoryChooser: " + dir);
+ instanceAvailable();
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(instance.context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PREF_DATA_FOLDER, dir);
+ editor.commit();
+ createImportDirectory();
+ }
+
+ /**
+ * Create a .nomedia file to prevent scanning by the media scanner.
+ */
+ private static void createNoMediaFile() {
+ File f = new File(instance.context.getExternalFilesDir(null),
+ ".nomedia");
+ if (!f.exists()) {
+ try {
+ f.createNewFile();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not create .nomedia file");
+ e.printStackTrace();
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, ".nomedia file created");
+ }
+ }
+
+ /**
+ * Creates the import directory if it doesn't exist and if storage is
+ * available
+ */
+ private static void createImportDirectory() {
+ File importDir = getDataFolder(instance.context,
+ IMPORT_DIR);
+ if (importDir != null) {
+ if (importDir.exists()) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Import directory already exists");
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Creating import directory");
+ importDir.mkdir();
+ }
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Could not access external storage.");
+ }
+ }
+
+ /**
+ * Updates alarm registered with the AlarmManager service or deactivates it.
+ */
+ public static void restartUpdateAlarm(long triggerAtMillis, long intervalMillis) {
+ instanceAvailable();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Restarting update alarm.");
+ AlarmManager alarmManager = (AlarmManager) instance.context
+ .getSystemService(Context.ALARM_SERVICE);
+ PendingIntent updateIntent = PendingIntent.getBroadcast(
+ instance.context, 0, new Intent(
+ FeedUpdateReceiver.ACTION_REFRESH_FEEDS), 0);
+ alarmManager.cancel(updateIntent);
+ if (intervalMillis != 0) {
+ alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtMillis, intervalMillis,
+ updateIntent);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Changed alarm to new interval");
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Automatic update was deactivated");
+ }
+ }
+
+
+ /**
+ * Reads episode cache size as it is saved in the episode_cache_size_values array.
+ */
+ public static int readEpisodeCacheSize(String valueFromPrefs) {
+ instanceAvailable();
+ return instance.readEpisodeCacheSizeInternal(valueFromPrefs);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java
new file mode 100644
index 000000000..84277b6d5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.core.receiver;
+
+import android.content.BroadcastReceiver;
+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.ClientConfig;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+/** Listens for events that make it necessary to reset the update alarm. */
+public class AlarmUpdateReceiver extends BroadcastReceiver {
+ private static final String TAG = "AlarmUpdateReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received intent");
+ if (StringUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Resetting update alarm after reboot");
+ } else if (StringUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Resetting update alarm after app upgrade");
+ }
+
+ ClientConfig.applicationCallbacks.setUpdateInterval(UserPreferences.getUpdateInterval());
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java
new file mode 100644
index 000000000..6ce30763d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.core.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.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBTasks;
+
+/** Refreshes all feeds when it receives an intent */
+public class FeedUpdateReceiver extends BroadcastReceiver {
+ private static final String TAG = "FeedUpdateReceiver";
+ public static final String ACTION_REFRESH_FEEDS = "de.danoeh.antennapod.feedupdatereceiver.refreshFeeds";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), ACTION_REFRESH_FEEDS)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received intent");
+ boolean mobileUpdate = UserPreferences.isAllowMobileUpdate();
+ if (mobileUpdate || connectedToWifi(context)) {
+ DBTasks.refreshExpiredFeeds(context);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Blocking automatic update: no wifi available / no mobile updates allowed");
+ }
+ }
+ }
+
+ private boolean connectedToWifi(Context context) {
+ ConnectivityManager connManager = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo mWifi = connManager
+ .getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+
+ return mWifi.isConnected();
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
new file mode 100644
index 000000000..a900248d2
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.core.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.KeyEvent;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+
+/** Receives media button events. */
+public class MediaButtonReceiver extends BroadcastReceiver {
+ private static final String TAG = "MediaButtonReceiver";
+ public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE";
+
+ public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Received intent");
+ KeyEvent event = (KeyEvent) intent.getExtras().get(
+ Intent.EXTRA_KEY_EVENT);
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ Intent serviceIntent = new Intent(context, PlaybackService.class);
+ int keycode = event.getKeyCode();
+ serviceIntent.putExtra(EXTRA_KEYCODE, keycode);
+ context.startService(serviceIntent);
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java
new file mode 100644
index 000000000..0f2a81dfb
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java
@@ -0,0 +1,251 @@
+package de.danoeh.antennapod.core.service;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException;
+import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+
+/**
+ * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument.
+ * This class also provides static methods for starting the GpodnetSyncService.
+ */
+public class GpodnetSyncService extends Service {
+ private static final String TAG = "GpodnetSyncService";
+
+ private static final long WAIT_INTERVAL = 5000L;
+
+ public static final String ARG_ACTION = "action";
+
+ public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync";
+
+ private GpodnetService service;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null;
+ if (action != null && action.equals(ACTION_SYNC)) {
+ Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
+ syncWaiterThread.restart();
+ } else {
+ Log.e(TAG, "Received invalid intent: action argument is null or invalid");
+ }
+ return START_FLAG_REDELIVERY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy");
+ syncWaiterThread.interrupt();
+
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private synchronized GpodnetService tryLogin() throws GpodnetServiceException {
+ if (service == null) {
+ service = new GpodnetService();
+ service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ }
+ return service;
+ }
+
+ private synchronized void syncChanges() {
+ if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) {
+ final long timestamp = GpodnetPreferences.getLastSyncTimestamp();
+ try {
+ final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this);
+ GpodnetService service = tryLogin();
+
+ if (timestamp == 0) {
+ // first sync: download all subscriptions...
+ GpodnetSubscriptionChange changes =
+ service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Downloaded subscription changes: " + changes);
+ processSubscriptionChanges(localSubscriptions, changes);
+
+ // ... then upload all local subscriptions
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Uploading subscription list: " + localSubscriptions);
+ GpodnetUploadChangesResponse uploadChangesResponse =
+ service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Uploading changes response: " + uploadChangesResponse);
+ GpodnetPreferences.removeAddedFeeds(localSubscriptions);
+ GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy());
+ GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
+ } else {
+ Set<String> added = GpodnetPreferences.getAddedFeedsCopy();
+ Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy();
+
+ // download remote changes first...
+ GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
+ processSubscriptionChanges(localSubscriptions, subscriptionChanges);
+
+ // ... then upload changes local changes
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
+ added.toString(), removed));
+ GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse);
+
+ GpodnetPreferences.removeAddedFeeds(added);
+ GpodnetPreferences.removeRemovedFeeds(removed);
+ GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
+ }
+ clearErrorNotifications();
+ } catch (GpodnetServiceException e) {
+ e.printStackTrace();
+ updateErrorNotification(e);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ }
+ }
+ stopSelf();
+ }
+
+ private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException {
+ for (String downloadUrl : changes.getAdded()) {
+ if (!localSubscriptions.contains(downloadUrl)) {
+ Feed feed = new Feed(downloadUrl, new Date());
+ DownloadRequester.getInstance().downloadFeed(this, feed);
+ }
+ }
+ for (String downloadUrl : changes.getRemoved()) {
+ DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl);
+ }
+ }
+
+ private void clearErrorNotifications() {
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ nm.cancel(R.id.notification_gpodnet_sync_error);
+ nm.cancel(R.id.notification_gpodnet_sync_autherror);
+ }
+
+ private void updateErrorNotification(GpodnetServiceException exception) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification");
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+ final String title;
+ final String description;
+ final int id;
+ if (exception instanceof GpodnetServiceAuthenticationException) {
+ title = getString(R.string.gpodnetsync_auth_error_title);
+ description = getString(R.string.gpodnetsync_auth_error_descr);
+ id = R.id.notification_gpodnet_sync_autherror;
+ } else {
+ title = getString(R.string.gpodnetsync_error_title);
+ description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage();
+ id = R.id.notification_gpodnet_sync_error;
+ }
+
+ PendingIntent activityIntent = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this);
+ Notification notification = builder.setContentTitle(title)
+ .setContentText(description)
+ .setContentIntent(activityIntent)
+ .setSmallIcon(R.drawable.stat_notify_sync_error)
+ .setAutoCancel(true)
+ .build();
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(id, notification);
+ }
+
+ private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) {
+ @Override
+ public void onWaitCompleted() {
+ syncChanges();
+ }
+ };
+
+ private abstract class WaiterThread {
+ private long waitInterval;
+ private Thread thread;
+
+ private WaiterThread(long waitInterval) {
+ this.waitInterval = waitInterval;
+ reinit();
+ }
+
+ public abstract void onWaitCompleted();
+
+ public void exec() {
+ if (!thread.isAlive()) {
+ thread.start();
+ }
+ }
+
+ private void reinit() {
+ if (thread != null && thread.isAlive()) {
+ Log.d(TAG, "Interrupting waiter thread");
+ thread.interrupt();
+ }
+ thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(waitInterval);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (!isInterrupted()) {
+ synchronized (this) {
+ onWaitCompleted();
+ }
+ }
+ }
+ };
+ }
+
+ public void restart() {
+ reinit();
+ exec();
+ }
+
+ public void interrupt() {
+ if (thread != null && thread.isAlive()) {
+ thread.interrupt();
+ }
+ }
+ }
+
+ public static void sendSyncIntent(Context context) {
+ if (GpodnetPreferences.loggedIn()) {
+ Intent intent = new Intent(context, GpodnetSyncService.class);
+ intent.putExtra(ARG_ACTION, ACTION_SYNC);
+ context.startService(intent);
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java
new file mode 100644
index 000000000..3efcf4da8
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java
@@ -0,0 +1,54 @@
+package de.danoeh.antennapod.core.service.download;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.impl.client.DefaultRedirectHandler;
+import org.apache.http.protocol.HttpContext;
+
+import java.net.URI;
+
+public class APRedirectHandler extends DefaultRedirectHandler {
+ // Identifier for logger
+ private static final String TAG = "APRedirectHandler";
+ // Header field, which has to be potentially fixed
+ private static final String LOC = "Location";
+ // Regular expressions for character strings, which should not appear in URLs
+ private static final String CHi[] = { "\\{", "\\}", "\\|", "\\\\", "\\^", "~", "\\[", "\\]", "\\`"};
+ private static final String CHo[] = { "%7B", "%7D", "%7C", "%5C", "%5E", "%7E", "%5B", "%5D", "%60"};
+
+ /**
+ * Workaround for broken URLs in redirection.
+ * Proper solution involves LaxRedirectStrategy() which is not available in
+ * current API yet.
+ */
+ @Override
+ public URI getLocationURI(HttpResponse response, HttpContext context)
+ throws org.apache.http.ProtocolException {
+
+ Header h[] = response.getHeaders(LOC);
+ if (h.length>0) {
+ String s = h[0].getValue();
+
+ // Fix broken URL
+ for(int i=0; i<CHi.length;i++)
+ s = s.replaceAll(CHi[i], CHo[i]);
+
+ // If anything had to be fixed, then replace the header
+ if (!s.equals(h[0].getValue()))
+ {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Original URL: " + h[0].getValue());
+
+ response.setHeader(LOC, s);
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Fixed URL: " + s);
+ }
+ }
+
+ // call DefaultRedirectHandler with fixed URL
+ return super.getLocationURI(response, context);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
new file mode 100644
index 000000000..67f059d7d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
@@ -0,0 +1,98 @@
+package de.danoeh.antennapod.core.service.download;
+
+import android.util.Log;
+
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnManagerPNames;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.AbstractHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.CoreProtocolPNames;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import java.util.concurrent.TimeUnit;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+
+/**
+ * Provides access to a HttpClient singleton.
+ */
+public class AntennapodHttpClient {
+ private static final String TAG = "AntennapodHttpClient";
+
+ public static final long EXPIRED_CONN_TIMEOUT_SEC = 30;
+
+ public static final int MAX_REDIRECTS = 5;
+ public static final int CONNECTION_TIMEOUT = 30000;
+ public static final int SOCKET_TIMEOUT = 30000;
+
+ public static final int MAX_CONNECTIONS = 8;
+
+
+ private static volatile HttpClient httpClient = null;
+
+ /**
+ * Returns the HttpClient singleton.
+ */
+ public static synchronized HttpClient getHttpClient() {
+ if (httpClient == null) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client");
+
+ HttpParams params = new BasicHttpParams();
+ params.setParameter(CoreProtocolPNames.USER_AGENT, ClientConfig.USER_AGENT);
+ params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS);
+ params.setBooleanParameter("http.protocol.reject-relative-redirect",
+ false);
+ HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
+ HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
+ HttpClientParams.setRedirecting(params, true);
+
+ httpClient = new DefaultHttpClient(createClientConnectionManager(), params);
+ // Workaround for broken URLs in redirection
+ ((AbstractHttpClient) httpClient)
+ .setRedirectHandler(new APRedirectHandler());
+ }
+ return httpClient;
+ }
+
+ /**
+ * Closes expired connections. This method should be called by the using class once has finished its work with
+ * the HTTP client.
+ */
+ public static synchronized void cleanup() {
+ if (httpClient != null) {
+ httpClient.getConnectionManager().closeExpiredConnections();
+ httpClient.getConnectionManager().closeIdleConnections(EXPIRED_CONN_TIMEOUT_SEC, TimeUnit.SECONDS);
+ }
+ }
+
+
+ private static ClientConnectionManager createClientConnectionManager() {
+ HttpParams params = new BasicHttpParams();
+ params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, MAX_CONNECTIONS);
+ return new ThreadSafeClientConnManager(params, prepareSchemeRegistry());
+ }
+
+ private static SchemeRegistry prepareSchemeRegistry() {
+ SchemeRegistry sr = new SchemeRegistry();
+
+ Scheme http = new Scheme("http",
+ PlainSocketFactory.getSocketFactory(), 80);
+ sr.register(http);
+ Scheme https = new Scheme("https",
+ SSLSocketFactory.getSocketFactory(), 443);
+ sr.register(https);
+
+ return sr;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java
new file mode 100644
index 000000000..c79da0a48
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java
@@ -0,0 +1,209 @@
+package de.danoeh.antennapod.core.service.download;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.apache.commons.lang3.Validate;
+
+public class DownloadRequest implements Parcelable {
+
+ private final String destination;
+ private final String source;
+ private final String title;
+ private String username;
+ private String password;
+ private boolean deleteOnFailure;
+ private final long feedfileId;
+ private final int feedfileType;
+
+ protected int progressPercent;
+ protected long soFar;
+ protected long size;
+ protected int statusMsg;
+
+ public DownloadRequest(String destination, String source, String title,
+ long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure) {
+ Validate.notNull(destination);
+ Validate.notNull(source);
+ Validate.notNull(title);
+
+ this.destination = destination;
+ this.source = source;
+ this.title = title;
+ this.feedfileId = feedfileId;
+ this.feedfileType = feedfileType;
+ this.username = username;
+ this.password = password;
+ this.deleteOnFailure = deleteOnFailure;
+ }
+
+ public DownloadRequest(String destination, String source, String title,
+ long feedfileId, int feedfileType) {
+ this(destination, source, title, feedfileId, feedfileType, null, null, true);
+ }
+
+ private DownloadRequest(Parcel in) {
+ destination = in.readString();
+ source = in.readString();
+ title = in.readString();
+ feedfileId = in.readLong();
+ feedfileType = in.readInt();
+ deleteOnFailure = (in.readByte() > 0);
+ if (in.dataAvail() > 0) {
+ username = in.readString();
+ } else {
+ username = null;
+ }
+ if (in.dataAvail() > 0) {
+ password = in.readString();
+ } else {
+ password = null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(destination);
+ dest.writeString(source);
+ dest.writeString(title);
+ dest.writeLong(feedfileId);
+ dest.writeInt(feedfileType);
+ dest.writeByte((deleteOnFailure) ? (byte) 1 : 0);
+ if (username != null) {
+ dest.writeString(username);
+ }
+ if (password != null) {
+ dest.writeString(password);
+ }
+ }
+
+ public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() {
+ public DownloadRequest createFromParcel(Parcel in) {
+ return new DownloadRequest(in);
+ }
+
+ public DownloadRequest[] newArray(int size) {
+ return new DownloadRequest[size];
+ }
+ };
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DownloadRequest that = (DownloadRequest) o;
+
+ if (deleteOnFailure != that.deleteOnFailure) return false;
+ if (feedfileId != that.feedfileId) return false;
+ if (feedfileType != that.feedfileType) return false;
+ if (progressPercent != that.progressPercent) return false;
+ if (size != that.size) return false;
+ if (soFar != that.soFar) return false;
+ if (statusMsg != that.statusMsg) return false;
+ if (destination != null ? !destination.equals(that.destination) : that.destination != null)
+ return false;
+ if (password != null ? !password.equals(that.password) : that.password != null)
+ return false;
+ if (source != null ? !source.equals(that.source) : that.source != null) return false;
+ if (title != null ? !title.equals(that.title) : that.title != null) return false;
+ if (username != null ? !username.equals(that.username) : that.username != null)
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = destination != null ? destination.hashCode() : 0;
+ result = 31 * result + (source != null ? source.hashCode() : 0);
+ result = 31 * result + (title != null ? title.hashCode() : 0);
+ result = 31 * result + (username != null ? username.hashCode() : 0);
+ result = 31 * result + (password != null ? password.hashCode() : 0);
+ result = 31 * result + (deleteOnFailure ? 1 : 0);
+ result = 31 * result + (int) (feedfileId ^ (feedfileId >>> 32));
+ result = 31 * result + feedfileType;
+ result = 31 * result + progressPercent;
+ result = 31 * result + (int) (soFar ^ (soFar >>> 32));
+ result = 31 * result + (int) (size ^ (size >>> 32));
+ result = 31 * result + statusMsg;
+ return result;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public long getFeedfileId() {
+ return feedfileId;
+ }
+
+ public int getFeedfileType() {
+ return feedfileType;
+ }
+
+ public int getProgressPercent() {
+ return progressPercent;
+ }
+
+ public void setProgressPercent(int progressPercent) {
+ this.progressPercent = progressPercent;
+ }
+
+ public long getSoFar() {
+ return soFar;
+ }
+
+ public void setSoFar(long soFar) {
+ this.soFar = soFar;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ public int getStatusMsg() {
+ return statusMsg;
+ }
+
+ public void setStatusMsg(int statusMsg) {
+ this.statusMsg = statusMsg;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public boolean isDeleteOnFailure() {
+ return deleteOnFailure;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
new file mode 100644
index 000000000..b8db5a387
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
@@ -0,0 +1,1208 @@
+package de.danoeh.antennapod.core.service.download;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.MediaMetadataRetriever;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.webkit.URLUtil;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.http.HttpStatus;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+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.feed.FeedPreferences;
+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.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.syndication.handler.FeedHandler;
+import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException;
+import de.danoeh.antennapod.core.util.ChapterUtils;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.core.util.InvalidFeedException;
+
+/**
+ * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent.
+ * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of
+ * the intent.
+ * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the
+ * type of the feedfile.
+ */
+public class DownloadService extends Service {
+ private static final String TAG = "DownloadService";
+
+ /**
+ * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the
+ * object whose download should be cancelled.
+ */
+ public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.core.service.cancelDownload";
+
+ /**
+ * Cancels all running downloads.
+ */
+ public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAllDownloads";
+
+ /**
+ * Extra for ACTION_CANCEL_DOWNLOAD
+ */
+ public static final String EXTRA_DOWNLOAD_URL = "downloadUrl";
+
+ /**
+ * Sent by the DownloadService when the content of the downloads list
+ * changes.
+ */
+ public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.core.service.downloadsContentChanged";
+
+ /**
+ * Extra for ACTION_ENQUEUE_DOWNLOAD intent.
+ */
+ public static final String EXTRA_REQUEST = "request";
+
+ /**
+ * Stores new media files that will be queued for auto-download if possible.
+ */
+ private List<Long> newMediaFiles;
+
+ /**
+ * Contains all completed downloads that have not been included in the report yet.
+ */
+ private List<DownloadStatus> reportQueue;
+
+ private ExecutorService syncExecutor;
+ private CompletionService<Downloader> downloadExecutor;
+ private FeedSyncThread feedSyncThread;
+
+ /**
+ * Number of threads of downloadExecutor.
+ */
+ private static final int NUM_PARALLEL_DOWNLOADS = 6;
+
+ private DownloadRequester requester;
+
+
+ private NotificationCompat.Builder notificationCompatBuilder;
+ private Notification.BigTextStyle notificationBuilder;
+ private int NOTIFICATION_ID = 2;
+ private int REPORT_ID = 3;
+
+ /**
+ * Currently running downloads.
+ */
+ private List<Downloader> downloads;
+
+ /**
+ * Number of running downloads.
+ */
+ private AtomicInteger numberOfDownloads;
+
+ /**
+ * True if service is running.
+ */
+ public static boolean isRunning = false;
+
+ private Handler handler;
+
+ private NotificationUpdater notificationUpdater;
+ private ScheduledFuture notificationUpdaterFuture;
+ private static final int SCHED_EX_POOL_SIZE = 1;
+ private ScheduledThreadPoolExecutor schedExecutor;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public DownloadService getService() {
+ return DownloadService.this;
+ }
+ }
+
+ private Thread downloadCompletionThread = new Thread() {
+ private static final String TAG = "downloadCompletionThread";
+
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started");
+ while (!isInterrupted()) {
+ try {
+ Downloader downloader = downloadExecutor.take().get();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received 'Download Complete' - message.");
+ removeDownload(downloader);
+ DownloadStatus status = downloader.getResult();
+ boolean successful = status.isSuccessful();
+
+ final int type = status.getFeedfileType();
+ if (successful) {
+ if (type == Feed.FEEDFILETYPE_FEED) {
+ handleCompletedFeedDownload(downloader
+ .getDownloadRequest());
+ } else if (type == FeedImage.FEEDFILETYPE_FEEDIMAGE) {
+ handleCompletedImageDownload(status, downloader.getDownloadRequest());
+ } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest());
+ }
+ } else {
+ numberOfDownloads.decrementAndGet();
+ if (!status.isCancelled()) {
+ if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) {
+ postAuthenticationNotification(downloader.getDownloadRequest());
+ } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
+ && Integer.valueOf(status.getReasonDetailed()) == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
+
+ Log.d(TAG, "Requested invalid range, restarting download from the beginning");
+ FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
+ DownloadRequester.getInstance().download(DownloadService.this, downloader.getDownloadRequest());
+ } else {
+ Log.e(TAG, "Download failed");
+ saveDownloadStatus(status);
+ handleFailedDownload(status, downloader.getDownloadRequest());
+ }
+ }
+ sendDownloadHandledIntent();
+ queryDownloadsAsync();
+ }
+ } catch (InterruptedException e) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted");
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ numberOfDownloads.decrementAndGet();
+ }
+ }
+ if (BuildConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread");
+ }
+ };
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent.getParcelableExtra(EXTRA_REQUEST) != null) {
+ onDownloadQueued(intent);
+ } else if (numberOfDownloads.get() == 0) {
+ stopSelf();
+ }
+ return Service.START_NOT_STICKY;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Service started");
+ isRunning = true;
+ handler = new Handler();
+ newMediaFiles = Collections.synchronizedList(new ArrayList<Long>());
+ reportQueue = Collections.synchronizedList(new ArrayList<DownloadStatus>());
+ downloads = new ArrayList<Downloader>();
+ numberOfDownloads = new AtomicInteger(0);
+
+ IntentFilter cancelDownloadReceiverFilter = new IntentFilter();
+ cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS);
+ cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD);
+ registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter);
+ syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ });
+ downloadExecutor = new ExecutorCompletionService<Downloader>(
+ Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS,
+ new ThreadFactory() {
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ }
+ )
+ );
+ schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE,
+ new ThreadFactory() {
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ }, new RejectedExecutionHandler() {
+
+ @Override
+ public void rejectedExecution(Runnable r,
+ ThreadPoolExecutor executor) {
+ Log.w(TAG, "SchedEx rejected submission of new task");
+ }
+ }
+ );
+ downloadCompletionThread.start();
+ feedSyncThread = new FeedSyncThread();
+ feedSyncThread.start();
+
+ setupNotificationBuilders();
+ requester = DownloadRequester.getInstance();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Service shutting down");
+ isRunning = false;
+
+ if (ClientConfig.downloadServiceCallbacks.shouldCreateReport()) {
+ updateReport();
+ }
+
+ stopForeground(true);
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ nm.cancel(NOTIFICATION_ID);
+
+ downloadCompletionThread.interrupt();
+ syncExecutor.shutdown();
+ schedExecutor.shutdown();
+ feedSyncThread.shutdown();
+ cancelNotificationUpdater();
+ unregisterReceiver(cancelDownloadReceiver);
+
+ if (!newMediaFiles.isEmpty()) {
+ DBTasks.autodownloadUndownloadedItems(getApplicationContext(),
+ ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()])));
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private void setupNotificationBuilders() {
+ Bitmap icon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.stat_notify_sync);
+
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+ notificationBuilder = new Notification.BigTextStyle(
+ new Notification.Builder(this).setOngoing(true)
+ .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)).setLargeIcon(icon)
+ .setSmallIcon(R.drawable.stat_notify_sync)
+ );
+ } else {
+ notificationCompatBuilder = new NotificationCompat.Builder(this)
+ .setOngoing(true).setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this))
+ .setLargeIcon(icon)
+ .setSmallIcon(R.drawable.stat_notify_sync);
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Notification set up");
+ }
+
+ /**
+ * Updates the contents of the service's notifications. Should be called
+ * before setupNotificationBuilders.
+ */
+ @SuppressLint("NewApi")
+ private Notification updateNotifications() {
+ String contentTitle = getString(R.string.download_notification_title);
+ int numDownloads = requester.getNumberOfDownloads();
+ String downloadsLeft;
+ if (numDownloads > 0) {
+ downloadsLeft = requester.getNumberOfDownloads()
+ + getString(R.string.downloads_left);
+ } else {
+ downloadsLeft = getString(R.string.downloads_processing);
+ }
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+
+ if (notificationBuilder != null) {
+
+ StringBuilder bigText = new StringBuilder("");
+ for (int i = 0; i < downloads.size(); i++) {
+ Downloader downloader = downloads.get(i);
+ final DownloadRequest request = downloader
+ .getDownloadRequest();
+ if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
+ if (request.getTitle() != null) {
+ if (i > 0) {
+ bigText.append("\n");
+ }
+ bigText.append("\u2022 " + request.getTitle());
+ }
+ } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ if (request.getTitle() != null) {
+ if (i > 0) {
+ bigText.append("\n");
+ }
+ bigText.append("\u2022 " + request.getTitle()
+ + " (" + request.getProgressPercent()
+ + "%)");
+ }
+ }
+
+ }
+ notificationBuilder.setSummaryText(downloadsLeft);
+ notificationBuilder.setBigContentTitle(contentTitle);
+ if (bigText != null) {
+ notificationBuilder.bigText(bigText.toString());
+ }
+ return notificationBuilder.build();
+ }
+ } else {
+ if (notificationCompatBuilder != null) {
+ notificationCompatBuilder.setContentTitle(contentTitle);
+ notificationCompatBuilder.setContentText(downloadsLeft);
+ return notificationCompatBuilder.build();
+ }
+ }
+ return null;
+ }
+
+ private Downloader getDownloader(String downloadUrl) {
+ for (Downloader downloader : downloads) {
+ if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) {
+ return downloader;
+ }
+ }
+ return null;
+ }
+
+ private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) {
+ String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL);
+ Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra");
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Cancelling download with url " + url);
+ Downloader d = getDownloader(url);
+ if (d != null) {
+ d.cancel();
+ } else {
+ Log.e(TAG, "Could not cancel download with url " + url);
+ }
+
+ } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) {
+ for (Downloader d : downloads) {
+ d.cancel();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Cancelled all downloads");
+ }
+ sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
+
+ }
+ queryDownloads();
+ }
+
+ };
+
+ private void onDownloadQueued(Intent intent) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received enqueue request");
+ DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST);
+ if (request == null) {
+ throw new IllegalArgumentException(
+ "ACTION_ENQUEUE_DOWNLOAD intent needs request extra");
+ }
+
+ Downloader downloader = getDownloader(request);
+ if (downloader != null) {
+ numberOfDownloads.incrementAndGet();
+ downloads.add(downloader);
+ downloadExecutor.submit(downloader);
+ sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
+ }
+
+ queryDownloads();
+ }
+
+ private Downloader getDownloader(DownloadRequest request) {
+ if (URLUtil.isHttpUrl(request.getSource())
+ || URLUtil.isHttpsUrl(request.getSource())) {
+ return new HttpDownloader(request);
+ }
+ Log.e(TAG,
+ "Could not find appropriate downloader for "
+ + request.getSource()
+ );
+ return null;
+ }
+
+ /**
+ * Remove download from the DownloadRequester list and from the
+ * DownloadService list.
+ */
+ private void removeDownload(final Downloader d) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Removing downloader: "
+ + d.getDownloadRequest().getSource());
+ boolean rc = downloads.remove(d);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Result of downloads.remove: " + rc);
+ DownloadRequester.getInstance().removeDownload(d.getDownloadRequest());
+ sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
+ }
+ });
+ }
+
+ /**
+ * Adds a new DownloadStatus object to the list of completed downloads and
+ * saves it in the database
+ *
+ * @param status the download that is going to be saved
+ */
+ private void saveDownloadStatus(DownloadStatus status) {
+ reportQueue.add(status);
+ DBWriter.addDownloadStatus(this, status);
+ }
+
+ private void sendDownloadHandledIntent() {
+ EventDistributor.getInstance().sendDownloadHandledBroadcast();
+ }
+
+ /**
+ * Creates a notification at the end of the service lifecycle to notify the
+ * user about the number of completed downloads. A report will only be
+ * created if the number of successfully downloaded feeds is bigger than 1
+ * or if there is at least one failed download which is not an image or if
+ * there is at least one downloaded media file.
+ */
+ private void updateReport() {
+ // check if report should be created
+ boolean createReport = false;
+ int successfulDownloads = 0;
+ int failedDownloads = 0;
+
+ // a download report is created if at least one download has failed
+ // (excluding failed image downloads)
+ for (DownloadStatus status : reportQueue) {
+ if (status.isSuccessful()) {
+ successfulDownloads++;
+ } else if (!status.isCancelled()) {
+ if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) {
+ createReport = true;
+ }
+ failedDownloads++;
+ }
+ }
+
+ if (createReport) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Creating report");
+ // create notification object
+ Notification notification = new NotificationCompat.Builder(this)
+ .setTicker(
+ getString(R.string.download_report_title))
+ .setContentTitle(
+ getString(R.string.download_report_title))
+ .setContentText(
+ String.format(
+ getString(R.string.download_report_content),
+ successfulDownloads, failedDownloads)
+ )
+ .setSmallIcon(R.drawable.stat_notify_sync)
+ .setLargeIcon(
+ BitmapFactory.decodeResource(getResources(),
+ R.drawable.stat_notify_sync)
+ )
+ .setContentIntent(
+ ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(this)
+ )
+ .setAutoCancel(true).build();
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(REPORT_ID, notification);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "No report is created");
+ }
+ reportQueue.clear();
+ }
+
+ /**
+ * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is
+ * used from a thread other than the main thread.
+ */
+ void queryDownloadsAsync() {
+ handler.post(new Runnable() {
+ public void run() {
+ queryDownloads();
+ ;
+ }
+ });
+ }
+
+ /**
+ * Check if there's something else to download, otherwise stop
+ */
+ void queryDownloads() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, numberOfDownloads.get() + " downloads left");
+ }
+
+ if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown");
+ stopSelf();
+ } else {
+ setupNotificationUpdater();
+ startForeground(NOTIFICATION_ID, updateNotifications());
+ }
+ }
+
+ private void postAuthenticationNotification(final DownloadRequest downloadRequest) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ final String resourceTitle = (downloadRequest.getTitle() != null)
+ ? downloadRequest.getTitle() : downloadRequest.getSource();
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this);
+ builder.setTicker(getText(R.string.authentication_notification_title))
+ .setContentTitle(getText(R.string.authentication_notification_title))
+ .setContentText(getText(R.string.authentication_notification_msg))
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg)
+ + ": " + resourceTitle))
+ .setSmallIcon(R.drawable.ic_stat_authentication)
+ .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication))
+ .setAutoCancel(true)
+ .setContentIntent(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(DownloadService.this, downloadRequest));
+ Notification n = builder.build();
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(downloadRequest.getSource().hashCode(), n);
+ }
+ });
+ }
+
+ /**
+ * Is called whenever a Feed is downloaded
+ */
+ private void handleCompletedFeedDownload(DownloadRequest request) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Handling completed Feed Download");
+ feedSyncThread.submitCompletedDownload(request);
+
+ }
+
+ /**
+ * Is called whenever a Feed-Image is downloaded
+ */
+ private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Handling completed Image Download");
+ syncExecutor.execute(new ImageHandlerThread(status, request));
+ }
+
+ /**
+ * Is called whenever a FeedMedia is downloaded.
+ */
+ private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Handling completed FeedMedia Download");
+ syncExecutor.execute(new MediaHandlerThread(status, request));
+ }
+
+ private void handleFailedDownload(DownloadStatus status, DownloadRequest request) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download");
+ syncExecutor.execute(new FailedDownloadHandler(status, request));
+ }
+
+ /**
+ * Takes a single Feed, parses the corresponding file and refreshes
+ * information in the manager
+ */
+ class FeedSyncThread extends Thread {
+ private static final String TAG = "FeedSyncThread";
+
+ private BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<DownloadRequest>();
+ private CompletionService<Feed> parserService = new ExecutorCompletionService<Feed>(Executors.newSingleThreadExecutor());
+ private ExecutorService dbService = Executors.newSingleThreadExecutor();
+ private Future<?> dbUpdateFuture;
+ private volatile boolean isActive = true;
+ private volatile boolean isCollectingRequests = false;
+
+ private final long WAIT_TIMEOUT = 3000;
+
+
+ /**
+ * Waits for completed requests. Once the first request has been taken, the method will wait WAIT_TIMEOUT ms longer to
+ * collect more completed requests.
+ *
+ * @return Collected feeds or null if the method has been interrupted during the first waiting period.
+ */
+ private List<Feed> collectCompletedRequests() {
+ List<Feed> results = new LinkedList<Feed>();
+ DownloadRequester requester = DownloadRequester.getInstance();
+ int tasks = 0;
+
+ try {
+ DownloadRequest request = completedRequests.take();
+ parserService.submit(new FeedParserTask(request));
+ tasks++;
+ } catch (InterruptedException e) {
+ return null;
+ }
+
+ tasks += pollCompletedDownloads();
+
+ isCollectingRequests = true;
+
+ if (requester.isDownloadingFeeds()) {
+ // wait for completion of more downloads
+ long startTime = System.currentTimeMillis();
+ long currentTime = startTime;
+ while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) {
+ try {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms");
+ sleep(startTime + WAIT_TIMEOUT - currentTime);
+ } catch (InterruptedException e) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "interrupted while waiting for more downloads");
+ tasks += pollCompletedDownloads();
+ } finally {
+ currentTime = System.currentTimeMillis();
+ }
+ }
+
+ tasks += pollCompletedDownloads();
+
+ }
+
+ isCollectingRequests = false;
+
+ for (int i = 0; i < tasks; i++) {
+ try {
+ Feed f = parserService.take().get();
+ if (f != null) {
+ results.add(f);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return results;
+ }
+
+ private int pollCompletedDownloads() {
+ int tasks = 0;
+ for (int i = 0; i < completedRequests.size(); i++) {
+ parserService.submit(new FeedParserTask(completedRequests.poll()));
+ tasks++;
+ }
+ return tasks;
+ }
+
+ @Override
+ public void run() {
+ while (isActive) {
+ final List<Feed> feeds = collectCompletedRequests();
+
+ if (feeds == null) {
+ continue;
+ }
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + feeds.size() + " feeds");
+
+ for (Feed feed : feeds) {
+ removeDuplicateImages(feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet.
+ }
+
+ // Save information of feed in DB
+ if (dbUpdateFuture != null) {
+ try {
+ dbUpdateFuture.get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ dbUpdateFuture = dbService.submit(new Runnable() {
+ @Override
+ public void run() {
+ Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, feeds.toArray(new Feed[feeds.size()]));
+
+ for (Feed savedFeed : savedFeeds) {
+ // Download Feed Image if provided and not downloaded
+ if (savedFeed.getImage() != null
+ && savedFeed.getImage().isDownloaded() == false) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Feed has image; Downloading....");
+ savedFeed.getImage().setOwner(savedFeed);
+ final Feed savedFeedRef = savedFeed;
+ try {
+ requester.downloadImage(DownloadService.this,
+ savedFeedRef.getImage());
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DBWriter.addDownloadStatus(
+ DownloadService.this,
+ new DownloadStatus(
+ savedFeedRef.getImage(),
+ savedFeedRef
+ .getImage()
+ .getHumanReadableIdentifier(),
+ DownloadError.ERROR_REQUEST_ERROR,
+ false, e.getMessage()
+ )
+ );
+ }
+ }
+
+ // queue new media files for automatic download
+ for (FeedItem item : savedFeed.getItems()) {
+ if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) {
+ newMediaFiles.add(item.getMedia().getId());
+ }
+ }
+
+ ClientConfig.downloadServiceCallbacks.onFeedParsed(DownloadService.this,
+ savedFeed);
+
+ numberOfDownloads.decrementAndGet();
+ }
+
+ sendDownloadHandledIntent();
+
+ queryDownloadsAsync();
+ }
+ });
+
+ }
+
+ if (dbUpdateFuture != null) {
+ try {
+ dbUpdateFuture.get();
+ } catch (InterruptedException e) {
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down");
+
+ }
+
+ private class FeedParserTask implements Callable<Feed> {
+
+ private DownloadRequest request;
+
+ private FeedParserTask(DownloadRequest request) {
+ this.request = request;
+ }
+
+ @Override
+ public Feed call() throws Exception {
+ return parseFeed(request);
+ }
+ }
+
+ private Feed parseFeed(DownloadRequest request) {
+ Feed savedFeed = null;
+
+ Feed feed = new Feed(request.getSource(), new Date());
+ feed.setFile_url(request.getDestination());
+ feed.setId(request.getFeedfileId());
+ feed.setDownloaded(true);
+ feed.setPreferences(new FeedPreferences(0, true, request.getUsername(), request.getPassword()));
+
+ DownloadError reason = null;
+ String reasonDetailed = null;
+ boolean successful = true;
+ FeedHandler feedHandler = new FeedHandler();
+
+ try {
+ feed = feedHandler.parseFeed(feed).feed;
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, feed.getTitle() + " parsed");
+ if (checkFeedData(feed) == false) {
+ throw new InvalidFeedException();
+ }
+
+ } catch (SAXException e) {
+ successful = false;
+ e.printStackTrace();
+ reason = DownloadError.ERROR_PARSER_EXCEPTION;
+ reasonDetailed = e.getMessage();
+ } catch (IOException e) {
+ successful = false;
+ e.printStackTrace();
+ reason = DownloadError.ERROR_PARSER_EXCEPTION;
+ reasonDetailed = e.getMessage();
+ } catch (ParserConfigurationException e) {
+ successful = false;
+ e.printStackTrace();
+ reason = DownloadError.ERROR_PARSER_EXCEPTION;
+ reasonDetailed = e.getMessage();
+ } catch (UnsupportedFeedtypeException e) {
+ e.printStackTrace();
+ successful = false;
+ reason = DownloadError.ERROR_UNSUPPORTED_TYPE;
+ reasonDetailed = e.getMessage();
+ } catch (InvalidFeedException e) {
+ e.printStackTrace();
+ successful = false;
+ reason = DownloadError.ERROR_PARSER_EXCEPTION;
+ reasonDetailed = e.getMessage();
+ }
+
+ // cleanup();
+ if (savedFeed == null) {
+ savedFeed = feed;
+ }
+
+
+ if (successful) {
+ return savedFeed;
+ } else {
+ numberOfDownloads.decrementAndGet();
+ saveDownloadStatus(new DownloadStatus(savedFeed,
+ savedFeed.getHumanReadableIdentifier(), reason, successful,
+ reasonDetailed));
+ return null;
+ }
+ }
+
+
+ /**
+ * Checks if the feed was parsed correctly.
+ */
+ private boolean checkFeedData(Feed feed) {
+ if (feed.getTitle() == null) {
+ Log.e(TAG, "Feed has no title.");
+ return false;
+ }
+ if (!hasValidFeedItems(feed)) {
+ Log.e(TAG, "Feed has invalid items");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the FeedItems of this feed have images that point
+ * to the same URL. If two FeedItems have an image that points to
+ * the same URL, the reference of the second item is removed, so that every image
+ * reference is unique.
+ */
+ private void removeDuplicateImages(Feed feed) {
+ for (int x = 0; x < feed.getItems().size(); x++) {
+ for (int y = x + 1; y < feed.getItems().size(); y++) {
+ FeedItem item1 = feed.getItems().get(x);
+ FeedItem item2 = feed.getItems().get(y);
+ if (item1.hasItemImage() && item2.hasItemImage()) {
+ if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) {
+ item2.setImage(null);
+ }
+ }
+ }
+ }
+ }
+
+ private boolean hasValidFeedItems(Feed feed) {
+ for (FeedItem item : feed.getItems()) {
+ if (item.getTitle() == null) {
+ Log.e(TAG, "Item has no title");
+ return false;
+ }
+ if (item.getPubDate() == null) {
+ Log.e(TAG,
+ "Item has no pubDate. Using current time as pubDate");
+ if (item.getTitle() != null) {
+ Log.e(TAG, "Title of invalid item: " + item.getTitle());
+ }
+ item.setPubDate(new Date());
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Delete files that aren't needed anymore
+ */
+ private void cleanup(Feed feed) {
+ if (feed.getFile_url() != null) {
+ if (new File(feed.getFile_url()).delete())
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Successfully deleted cache file.");
+ else
+ Log.e(TAG, "Failed to delete cache file.");
+ feed.setFile_url(null);
+ } else if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Didn't delete cache file: File url is not set.");
+ }
+ }
+
+ public void shutdown() {
+ isActive = false;
+ if (isCollectingRequests) {
+ interrupt();
+ }
+ }
+
+ public void submitCompletedDownload(DownloadRequest request) {
+ completedRequests.offer(request);
+ if (isCollectingRequests) {
+ interrupt();
+ }
+ }
+
+ }
+
+ /**
+ * Handles failed downloads.
+ * <p/>
+ * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location
+ * of the downloaded file.
+ * <p/>
+ * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails.
+ */
+ class FailedDownloadHandler implements Runnable {
+
+ private DownloadRequest request;
+ private DownloadStatus status;
+
+ FailedDownloadHandler(DownloadStatus status, DownloadRequest request) {
+ this.request = request;
+ this.status = status;
+ }
+
+ @Override
+ public void run() {
+ if (request.isDeleteOnFailure()) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true");
+ } else {
+ File dest = new File(request.getDestination());
+ if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ Log.d(TAG, "File has been partially downloaded. Writing file url");
+ FeedMedia media = DBReader.getFeedMedia(DownloadService.this, request.getFeedfileId());
+ media.setFile_url(request.getDestination());
+ try {
+ DBWriter.setFeedMedia(DownloadService.this, media).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles a completed image download.
+ */
+ class ImageHandlerThread implements Runnable {
+
+ private DownloadRequest request;
+ private DownloadStatus status;
+
+ public ImageHandlerThread(DownloadStatus status, DownloadRequest request) {
+ Validate.notNull(status);
+ Validate.notNull(request);
+
+ this.status = status;
+ this.request = request;
+ }
+
+ @Override
+ public void run() {
+ FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId());
+ if (image == null) {
+ throw new IllegalStateException("Could not find downloaded image in database");
+ }
+
+ image.setFile_url(request.getDestination());
+ image.setDownloaded(true);
+
+ saveDownloadStatus(status);
+ sendDownloadHandledIntent();
+ DBWriter.setFeedImage(DownloadService.this, image);
+ numberOfDownloads.decrementAndGet();
+ queryDownloadsAsync();
+ }
+ }
+
+ /**
+ * Handles a completed media download.
+ */
+ class MediaHandlerThread implements Runnable {
+
+ private DownloadRequest request;
+ private DownloadStatus status;
+
+ public MediaHandlerThread(DownloadStatus status, DownloadRequest request) {
+ Validate.notNull(status);
+ Validate.notNull(request);
+
+ this.status = status;
+ this.request = request;
+ }
+
+ @Override
+ public void run() {
+ FeedMedia media = DBReader.getFeedMedia(DownloadService.this,
+ request.getFeedfileId());
+ if (media == null) {
+ throw new IllegalStateException(
+ "Could not find downloaded media object in database");
+ }
+ boolean chaptersRead = false;
+ media.setDownloaded(true);
+ media.setFile_url(request.getDestination());
+
+ // Get duration
+ MediaMetadataRetriever mmr = null;
+ try {
+ mmr = new MediaMetadataRetriever();
+ mmr.setDataSource(media.getFile_url());
+ String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+ media.setDuration(Integer.parseInt(durationStr));
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Duration of file is " + media.getDuration());
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ } finally {
+ if (mmr != null) {
+ mmr.release();
+ }
+ }
+
+ if (media.getItem().getChapters() == null) {
+ ChapterUtils.loadChaptersFromFileUrl(media);
+ if (media.getItem().getChapters() != null) {
+ chaptersRead = true;
+ }
+ }
+
+ try {
+ if (chaptersRead) {
+ DBWriter.setFeedItem(DownloadService.this, media.getItem()).get();
+ }
+ DBWriter.setFeedMedia(DownloadService.this, media).get();
+ if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) {
+ DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get();
+ }
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage());
+ }
+
+ saveDownloadStatus(status);
+ sendDownloadHandledIntent();
+
+ numberOfDownloads.decrementAndGet();
+ queryDownloadsAsync();
+ }
+ }
+
+ /**
+ * Schedules the notification updater task if it hasn't been scheduled yet.
+ */
+ private void setupNotificationUpdater() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Setting up notification updater");
+ if (notificationUpdater == null) {
+ notificationUpdater = new NotificationUpdater();
+ notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate(
+ notificationUpdater, 5L, 5L, TimeUnit.SECONDS);
+ }
+ }
+
+ private void cancelNotificationUpdater() {
+ boolean result = false;
+ if (notificationUpdaterFuture != null) {
+ result = notificationUpdaterFuture.cancel(true);
+ }
+ notificationUpdater = null;
+ notificationUpdaterFuture = null;
+ Log.d(TAG, "NotificationUpdater cancelled. Result: " + result);
+ }
+
+ private class NotificationUpdater implements Runnable {
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Notification n = updateNotifications();
+ if (n != null) {
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(NOTIFICATION_ID, n);
+ }
+ }
+ });
+ }
+ }
+
+ public List<Downloader> getDownloads() {
+ return downloads;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java
new file mode 100644
index 000000000..d05650d10
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java
@@ -0,0 +1,181 @@
+package de.danoeh.antennapod.core.service.download;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.core.feed.FeedFile;
+import de.danoeh.antennapod.core.util.DownloadError;
+
+import java.util.Date;
+
+/** Contains status attributes for one download */
+public class DownloadStatus {
+ /**
+ * Downloaders should use this constant for the size attribute if necessary
+ * so that the listadapters etc. can react properly.
+ */
+ public static final int SIZE_UNKNOWN = -1;
+
+ // ----------------------------------- ATTRIBUTES STORED IN DB
+ /** Unique id for storing the object in database. */
+ protected long id;
+ /**
+ * A human-readable string which is shown to the user so that he can
+ * identify the download. Should be the title of the item/feed/media or the
+ * URL if the download has no other title.
+ */
+ protected String title;
+ protected DownloadError reason;
+ /**
+ * A message which can be presented to the user to give more information.
+ * Should be null if Download was successful.
+ */
+ protected String reasonDetailed;
+ protected boolean successful;
+ protected Date completionDate;
+ protected long feedfileId;
+ /**
+ * Is used to determine the type of the feedfile even if the feedfile does
+ * not exist anymore. The value should be FEEDFILETYPE_FEED,
+ * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA
+ */
+ protected int feedfileType;
+
+ // ------------------------------------ NOT STORED IN DB
+ protected boolean done;
+ protected boolean cancelled;
+
+ /** Constructor for restoring Download status entries from DB. */
+ public DownloadStatus(long id, String title, long feedfileId,
+ int feedfileType, boolean successful, DownloadError reason,
+ Date completionDate, String reasonDetailed) {
+ this.id = id;
+ this.title = title;
+ this.done = true;
+ this.feedfileId = feedfileId;
+ this.reason = reason;
+ this.successful = successful;
+ this.completionDate = (Date) completionDate.clone();
+ this.reasonDetailed = reasonDetailed;
+ this.feedfileType = feedfileType;
+ }
+
+ public DownloadStatus(DownloadRequest request, DownloadError reason,
+ boolean successful, boolean cancelled, String reasonDetailed) {
+ Validate.notNull(request);
+
+ this.title = request.getTitle();
+ this.feedfileId = request.getFeedfileId();
+ this.feedfileType = request.getFeedfileType();
+ this.reason = reason;
+ this.successful = successful;
+ this.cancelled = cancelled;
+ this.reasonDetailed = reasonDetailed;
+ this.completionDate = new Date();
+ }
+
+ /** Constructor for creating new completed downloads. */
+ public DownloadStatus(FeedFile feedfile, String title, DownloadError reason,
+ boolean successful, String reasonDetailed) {
+ Validate.notNull(feedfile);
+
+ this.title = title;
+ this.done = true;
+ this.feedfileId = feedfile.getId();
+ this.feedfileType = feedfile.getTypeAsInt();
+ this.reason = reason;
+ this.successful = successful;
+ this.completionDate = new Date();
+ this.reasonDetailed = reasonDetailed;
+ }
+
+ /** Constructor for creating new completed downloads. */
+ public DownloadStatus(long feedfileId, int feedfileType, String title,
+ DownloadError reason, boolean successful, String reasonDetailed) {
+ this.title = title;
+ this.done = true;
+ this.feedfileId = feedfileId;
+ this.feedfileType = feedfileType;
+ this.reason = reason;
+ this.successful = successful;
+ this.completionDate = new Date();
+ this.reasonDetailed = reasonDetailed;
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadStatus [id=" + id + ", title=" + title + ", reason="
+ + reason + ", reasonDetailed=" + reasonDetailed
+ + ", successful=" + successful + ", completionDate="
+ + completionDate + ", feedfileId=" + feedfileId
+ + ", feedfileType=" + feedfileType + ", done=" + done
+ + ", cancelled=" + cancelled + "]";
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public DownloadError getReason() {
+ return reason;
+ }
+
+ public String getReasonDetailed() {
+ return reasonDetailed;
+ }
+
+ public boolean isSuccessful() {
+ return successful;
+ }
+
+ public Date getCompletionDate() {
+ return (Date) completionDate.clone();
+ }
+
+ public long getFeedfileId() {
+ return feedfileId;
+ }
+
+ public int getFeedfileType() {
+ return feedfileType;
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ public void setSuccessful() {
+ this.successful = true;
+ this.reason = DownloadError.SUCCESS;
+ this.done = true;
+ }
+
+ public void setFailed(DownloadError reason, String reasonDetailed) {
+ this.successful = false;
+ this.reason = reason;
+ this.reasonDetailed = reasonDetailed;
+ this.done = true;
+ }
+
+ public void setCancelled() {
+ this.successful = false;
+ this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED;
+ this.done = true;
+ this.cancelled = true;
+ }
+
+ public void setCompletionDate(Date completionDate) {
+ this.completionDate = (Date) completionDate.clone();
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java
new file mode 100644
index 000000000..d8042d202
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java
@@ -0,0 +1,73 @@
+package de.danoeh.antennapod.core.service.download;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+
+import java.util.concurrent.Callable;
+
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+
+/**
+ * Downloads files
+ */
+public abstract class Downloader implements Callable<Downloader> {
+ private static final String TAG = "Downloader";
+
+ protected volatile boolean finished;
+
+ protected volatile boolean cancelled;
+
+ protected DownloadRequest request;
+ protected DownloadStatus result;
+
+ public Downloader(DownloadRequest request) {
+ super();
+ this.request = request;
+ this.request.setStatusMsg(R.string.download_pending);
+ this.cancelled = false;
+ this.result = new DownloadStatus(request, null, false, false, null);
+ }
+
+ protected abstract void download();
+
+ public final Downloader call() {
+ WifiManager wifiManager = (WifiManager)
+ ClientConfig.applicationCallbacks.getApplicationInstance().getSystemService(Context.WIFI_SERVICE);
+ WifiManager.WifiLock wifiLock = null;
+ if (wifiManager != null) {
+ wifiLock = wifiManager.createWifiLock(TAG);
+ wifiLock.acquire();
+ }
+
+ download();
+
+ if (wifiLock != null) {
+ wifiLock.release();
+ }
+
+ if (result == null) {
+ throw new IllegalStateException(
+ "Downloader hasn't created DownloadStatus object");
+ }
+ finished = true;
+ return this;
+ }
+
+ public DownloadRequest getDownloadRequest() {
+ return request;
+ }
+
+ public DownloadStatus getResult() {
+ return result;
+ }
+
+ public boolean isFinished() {
+ return finished;
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java
new file mode 100644
index 000000000..2d9347b0a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java
@@ -0,0 +1,10 @@
+package de.danoeh.antennapod.core.service.download;
+
+/**
+ * Callback used by the Downloader-classes to notify the requester that the
+ * download has completed.
+ */
+public interface DownloaderCallback {
+
+ public void onDownloadCompleted(Downloader downloader);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
new file mode 100644
index 000000000..32d0d351a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
@@ -0,0 +1,252 @@
+package de.danoeh.antennapod.core.service.download;
+
+import android.net.http.AndroidHttpClient;
+import android.util.Log;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.message.BasicHeader;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.FeedImage;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.core.util.URIUtil;
+
+public class HttpDownloader extends Downloader {
+ private static final String TAG = "HttpDownloader";
+
+ private static final int BUFFER_SIZE = 8 * 1024;
+
+ public HttpDownloader(DownloadRequest request) {
+ super(request);
+ }
+
+ @Override
+ protected void download() {
+ File destination = new File(request.getDestination());
+ final boolean fileExists = destination.exists();
+
+ if (request.isDeleteOnFailure() && fileExists) {
+ Log.w(TAG, "File already exists");
+ if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) {
+ onFail(DownloadError.ERROR_FILE_EXISTS, null);
+ return;
+ } else {
+ onSuccess();
+ return;
+ }
+ }
+
+ HttpClient httpClient = AntennapodHttpClient.getHttpClient();
+ RandomAccessFile out = null;
+ InputStream connection = null;
+ try {
+ HttpGet httpGet = new HttpGet(URIUtil.getURIFromRequestUrl(request.getSource()));
+
+ // add authentication information
+ String userInfo = httpGet.getURI().getUserInfo();
+ if (userInfo != null) {
+ String[] parts = userInfo.split(":");
+ if (parts.length == 2) {
+ httpGet.addHeader(BasicScheme.authenticate(
+ new UsernamePasswordCredentials(parts[0], parts[1]),
+ "UTF-8", false));
+ }
+ } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
+ httpGet.addHeader(BasicScheme.authenticate(new UsernamePasswordCredentials(request.getUsername(),
+ request.getPassword()), "UTF-8", false));
+ }
+
+ // add range header if necessary
+ if (fileExists) {
+ request.setSoFar(destination.length());
+ httpGet.addHeader(new BasicHeader("Range",
+ "bytes=" + request.getSoFar() + "-"));
+ if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar());
+ }
+
+ HttpResponse response = httpClient.execute(httpGet);
+ HttpEntity httpEntity = response.getEntity();
+ int responseCode = response.getStatusLine().getStatusCode();
+ Header contentEncodingHeader = response.getFirstHeader("Content-Encoding");
+
+ final boolean isGzip = contentEncodingHeader != null &&
+ contentEncodingHeader.getValue().equalsIgnoreCase("gzip");
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Response code is " + responseCode);
+
+ if (responseCode / 100 != 2 || httpEntity == null) {
+ final DownloadError error;
+ final String details;
+ if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ error = DownloadError.ERROR_UNAUTHORIZED;
+ details = String.valueOf(responseCode);
+ } else {
+ error = DownloadError.ERROR_HTTP_DATA_ERROR;
+ details = String.valueOf(responseCode);
+ }
+ onFail(error, details);
+ return;
+ }
+
+ if (!StorageUtils.storageAvailable(ClientConfig.applicationCallbacks.getApplicationInstance())) {
+ onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null);
+ return;
+ }
+
+ connection = new BufferedInputStream(AndroidHttpClient
+ .getUngzippedContent(httpEntity));
+
+ Header[] contentRangeHeaders = (fileExists) ? response.getHeaders("Content-Range") : null;
+
+ if (fileExists && responseCode == HttpStatus.SC_PARTIAL_CONTENT
+ && contentRangeHeaders != null && contentRangeHeaders.length > 0) {
+ String start = contentRangeHeaders[0].getValue().substring("bytes ".length(),
+ contentRangeHeaders[0].getValue().indexOf("-"));
+ request.setSoFar(Long.valueOf(start));
+ Log.d(TAG, "Starting download at position " + request.getSoFar());
+
+ out = new RandomAccessFile(destination, "rw");
+ out.seek(request.getSoFar());
+ } else {
+ destination.delete();
+ destination.createNewFile();
+ out = new RandomAccessFile(destination, "rw");
+ }
+
+
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int count = 0;
+ request.setStatusMsg(R.string.download_running);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Getting size of download");
+ request.setSize(httpEntity.getContentLength() + request.getSoFar());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Size is " + request.getSize());
+ if (request.getSize() < 0) {
+ request.setSize(DownloadStatus.SIZE_UNKNOWN);
+ }
+
+ long freeSpace = StorageUtils.getFreeSpaceAvailable();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Free space is " + freeSpace);
+
+ if (request.getSize() != DownloadStatus.SIZE_UNKNOWN
+ && request.getSize() > freeSpace) {
+ onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null);
+ return;
+ }
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting download");
+ while (!cancelled
+ && (count = connection.read(buffer)) != -1) {
+ out.write(buffer, 0, count);
+ request.setSoFar(request.getSoFar() + count);
+ request.setProgressPercent((int) (((double) request
+ .getSoFar() / (double) request
+ .getSize()) * 100));
+ }
+ if (cancelled) {
+ onCancelled();
+ } else {
+ // check if size specified in the response header is the same as the size of the
+ // written file. This check cannot be made if compression was used
+ if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN &&
+ request.getSoFar() != request.getSize()) {
+ onFail(DownloadError.ERROR_IO_ERROR,
+ "Download completed but size: " +
+ request.getSoFar() +
+ " does not equal expected size " +
+ request.getSize()
+ );
+ return;
+ }
+ onSuccess();
+ }
+
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage());
+ } catch (SocketTimeoutException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage());
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage());
+ } catch (IOException e) {
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_IO_ERROR, e.getMessage());
+ } catch (NullPointerException e) {
+ // might be thrown by connection.getInputStream()
+ e.printStackTrace();
+ onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource());
+ } finally {
+ IOUtils.closeQuietly(out);
+ AntennapodHttpClient.cleanup();
+ }
+ }
+
+ private void onSuccess() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Download was successful");
+ result.setSuccessful();
+ }
+
+ private void onFail(DownloadError reason, String reasonDetailed) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Download failed");
+ }
+ result.setFailed(reason, reasonDetailed);
+ if (request.isDeleteOnFailure()) {
+ cleanup();
+ }
+ }
+
+ private void onCancelled() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Download was cancelled");
+ result.setCancelled();
+ cleanup();
+ }
+
+ /**
+ * Deletes unfinished downloads.
+ */
+ private void cleanup() {
+ if (request.getDestination() != null) {
+ File dest = new File(request.getDestination());
+ if (dest.exists()) {
+ boolean rc = dest.delete();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Deleted file " + dest.getName() + "; Result: "
+ + rc);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "cleanup() didn't delete file: does not exist.");
+ }
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
new file mode 100644
index 000000000..1261c21fe
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
@@ -0,0 +1,1141 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.RemoteControlClient;
+import android.media.RemoteControlClient.MetadataEditor;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.SurfaceHolder;
+import android.widget.Toast;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+import java.util.List;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.asynctask.PicassoProvider;
+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.feed.MediaType;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+/**
+ * Controls the MediaPlayer that plays a FeedMedia-file
+ */
+public class PlaybackService extends Service {
+ public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE";
+ public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE";
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "PlaybackService";
+
+ /**
+ * Parcelable of type Playable.
+ */
+ public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
+ /**
+ * True if media should be streamed.
+ */
+ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream";
+ /**
+ * True if playback should be started immediately after media has been
+ * prepared.
+ */
+ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
+
+ public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately";
+
+ public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged";
+ public static final String EXTRA_NEW_PLAYER_STATUS = "extra.de.danoeh.antennapod.service.playerStatusChanged.newStatus";
+ private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
+ private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged";
+
+ public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.core.service.playerNotification";
+ public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode";
+ public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType";
+
+ /**
+ * If the PlaybackService receives this action, it will stop playback and
+ * try to shutdown.
+ */
+ public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService";
+
+ /**
+ * If the PlaybackService receives this action, it will end playback of the
+ * current episode and load the next episode if there is one available.
+ */
+ public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode";
+
+ /**
+ * Used in NOTIFICATION_TYPE_RELOAD.
+ */
+ public static final int EXTRA_CODE_AUDIO = 1;
+ public static final int EXTRA_CODE_VIDEO = 2;
+
+ public static final int NOTIFICATION_TYPE_ERROR = 0;
+ public static final int NOTIFICATION_TYPE_INFO = 1;
+ public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
+
+ /**
+ * Receivers of this intent should update their information about the curently playing media
+ */
+ public static final int NOTIFICATION_TYPE_RELOAD = 3;
+ /**
+ * The state of the sleeptimer changed.
+ */
+ public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
+ public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
+ public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
+ /**
+ * No more episodes are going to be played.
+ */
+ public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
+
+ /**
+ * Playback speed has changed
+ */
+ public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8;
+
+ /**
+ * Returned by getPositionSafe() or getDurationSafe() if the playbackService
+ * is in an invalid state.
+ */
+ public static final int INVALID_TIME = -1;
+
+ /**
+ * Is true if service is running.
+ */
+ public static boolean isRunning = false;
+ /**
+ * Is true if service has received a valid start command.
+ */
+ public static boolean started = false;
+
+ private static final int NOTIFICATION_ID = 1;
+
+ private RemoteControlClient remoteControlClient;
+ private PlaybackServiceMediaPlayer mediaPlayer;
+ private PlaybackServiceTaskManager taskManager;
+
+ private static volatile MediaType currentMediaType = MediaType.UNKNOWN;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public PlaybackService getService() {
+ return PlaybackService.this;
+ }
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received onUnbind event");
+ return super.onUnbind(intent);
+ }
+
+ /**
+ * Returns an intent which starts an audio- or videoplayer, depending on the
+ * type of media that is being played. If the playbackservice is not
+ * running, the type of the last played media will be looked up.
+ */
+ public static Intent getPlayerActivityIntent(Context context) {
+ if (isRunning) {
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType);
+ } else {
+ if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO);
+ } else {
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO);
+ }
+ }
+ }
+
+ /**
+ * Same as getPlayerActivityIntent(context), but here the type of activity
+ * depends on the FeedMedia that is provided as an argument.
+ */
+ public static Intent getPlayerActivityIntent(Context context, Playable media) {
+ MediaType mt = media.getMediaType();
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt);
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Service created.");
+ isRunning = true;
+
+ registerReceiver(headsetDisconnected, new IntentFilter(
+ Intent.ACTION_HEADSET_PLUG));
+ registerReceiver(shutdownReceiver, new IntentFilter(
+ ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ registerReceiver(audioBecomingNoisy, new IntentFilter(
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
+ ACTION_SKIP_CURRENT_EPISODE));
+ remoteControlClient = setupRemoteControlClient();
+ taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
+ mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback);
+
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Service is about to be destroyed");
+ isRunning = false;
+ started = false;
+ currentMediaType = MediaType.UNKNOWN;
+
+ unregisterReceiver(headsetDisconnected);
+ unregisterReceiver(shutdownReceiver);
+ unregisterReceiver(audioBecomingNoisy);
+ unregisterReceiver(skipCurrentEpisodeReceiver);
+ mediaPlayer.shutdown();
+ taskManager.shutdown();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received onBind event");
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "OnStartCommand called");
+ final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
+ final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
+ if (keycode == -1 && playable == null) {
+ Log.e(TAG, "PlaybackService was started with no arguments");
+ stopSelf();
+ }
+
+ if ((flags & Service.START_FLAG_REDELIVERY) != 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.");
+ stopForeground(true);
+ } else {
+
+ if (keycode != -1) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received media button event");
+ handleKeycode(keycode);
+ } else {
+ started = true;
+ boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
+ true);
+ boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
+ boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
+ }
+ }
+
+ return Service.START_REDELIVER_INTENT;
+ }
+
+ /**
+ * Handles media button events
+ */
+ private void handleKeycode(int keycode) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Handling keycode: " + keycode);
+
+ final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
+ final PlayerStatus status = info.playerStatus;
+ switch (keycode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ if (UserPreferences.isPersistNotify()) {
+ mediaPlayer.pause(false, true);
+ }
+ else {
+ mediaPlayer.pause(true, true);
+ }
+ } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.PREPARING) {
+ mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared());
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ if (UserPreferences.isPersistNotify()) {
+ mediaPlayer.pause(false, true);
+ }
+ else {
+ mediaPlayer.pause(true, true);
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs());
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs());
+ break;
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, true);
+ }
+ stopForeground(true); // gets rid of persistent notification
+ break;
+ default:
+ if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something
+ String message = String.format(getResources().getString(R.string.unknown_media_key), keycode);
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Called by a mediaplayer Activity as soon as it has prepared its
+ * mediaplayer.
+ */
+ public void setVideoSurface(SurfaceHolder sh) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Setting display");
+ mediaPlayer.setVideoSurface(sh);
+ }
+
+ /**
+ * Called when the surface holder of the mediaplayer has to be changed.
+ */
+ private void resetVideoSurface() {
+ taskManager.cancelPositionSaver();
+ mediaPlayer.resetVideoSurface();
+ }
+
+ public void notifyVideoSurfaceAbandoned() {
+ stopForeground(true);
+ mediaPlayer.resetVideoSurface();
+ }
+
+ private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+ saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL);
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+ mediaPlayer.pause(true, true);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+
+ @Override
+ public void onWidgetUpdaterTick() {
+ updateWidget();
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ }
+ };
+
+ private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ currentMediaType = mediaPlayer.getCurrentMediaType();
+ switch (newInfo.playerStatus) {
+ case INITIALIZED:
+ writePlaybackPreferences();
+ break;
+
+ case PREPARED:
+ taskManager.startChapterLoader(newInfo.playable);
+ break;
+
+ case PAUSED:
+ taskManager.cancelPositionSaver();
+ saveCurrentPosition(false, 0);
+ taskManager.cancelWidgetUpdater();
+ if (UserPreferences.isPersistNotify() && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ // do not remove notification on pause based on user pref and whether android version supports expanded notifications
+ }
+ else {
+ // remove notifcation on pause
+ stopForeground(true);
+ }
+ break;
+
+ case STOPPED:
+ //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ //stopSelf();
+ break;
+
+ case PLAYING:
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Audiofocus successfully requested");
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Resuming/Starting playback");
+
+ taskManager.startPositionSaver();
+ taskManager.startWidgetUpdater();
+ setupNotification(newInfo);
+ break;
+ case ERROR:
+ writePlaybackPreferencesNoMediaPlaying();
+ break;
+
+ }
+
+ Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED);
+ statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal());
+ sendBroadcast(statusUpdate);
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ updateWidget();
+ refreshRemoteControlClientState(newInfo);
+ bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
+ bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
+ }
+
+ @Override
+ public void shouldStop() {
+ stopSelf();
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+ sendNotificationBroadcast(
+ NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ switch (code) {
+ case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
+ return true;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ final String TAG = "PlaybackService.onErrorListener";
+ Log.w(TAG, "An error has occured: " + what + " " + extra);
+ if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, false);
+ }
+ sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
+ writePlaybackPreferencesNoMediaPlaying();
+ stopSelf();
+ return true;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ PlaybackService.this.endPlayback(true);
+ return true;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return remoteControlClient;
+ }
+ };
+
+ private void endPlayback(boolean playNextEpisode) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Playback ended");
+
+ final Playable media = mediaPlayer.getPSMPInfo().playable;
+ if (media == null) {
+ Log.e(TAG, "Cannot end playback: media was null");
+ return;
+ }
+
+ taskManager.cancelPositionSaver();
+
+ boolean isInQueue = false;
+ FeedItem nextItem = null;
+
+ if (media instanceof FeedMedia) {
+ FeedItem item = ((FeedMedia) media).getItem();
+ DBWriter.markItemRead(PlaybackService.this, item, true, true);
+
+ try {
+ final List<FeedItem> queue = taskManager.getQueue();
+ isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
+ nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ // isInQueue remains false
+ }
+ if (isInQueue) {
+ DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
+ }
+ DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
+
+ // auto-flattr if enabled
+ if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) {
+ DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item);
+ }
+ }
+
+ // Load next episode if previous episode was in the queue and if there
+ // is an episode in the queue left.
+ // Start playback immediately if continuous playback is enabled
+ Playable nextMedia = null;
+ boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() &&
+ isInQueue &&
+ nextItem != null;
+
+ playNextEpisode = playNextEpisode &&
+ loadNextItem &&
+ UserPreferences.isFollowQueue();
+
+ if (loadNextItem) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading next item in queue");
+ nextMedia = nextItem.getMedia();
+ }
+ final boolean prepareImmediately;
+ final boolean startWhenPrepared;
+ final boolean stream;
+
+ if (playNextEpisode) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Playback of next episode will start immediately.");
+ prepareImmediately = startWhenPrepared = true;
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "No more episodes available to play");
+
+ prepareImmediately = startWhenPrepared = false;
+ stopForeground(true);
+ stopWidgetUpdater();
+ }
+
+ writePlaybackPreferencesNoMediaPlaying();
+ if (nextMedia != null) {
+ stream = !media.localFileAvailable();
+ mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
+ } else {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
+ mediaPlayer.stop();
+ //stopSelf();
+ }
+ }
+
+ public void setSleepTimer(long waitingTime) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+ + " milliseconds");
+ taskManager.setSleepTimer(waitingTime);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ public void disableSleepTimer() {
+ taskManager.disableSleepTimer();
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ private void writePlaybackPreferencesNoMediaPlaying() {
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.commit();
+ }
+
+
+ private void writePlaybackPreferences() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Writing playback preferences");
+
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
+ MediaType mediaType = mediaPlayer.getCurrentMediaType();
+ boolean stream = mediaPlayer.isStreaming();
+
+ if (info.playable != null) {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ info.playable.getPlayableType());
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
+ stream);
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
+ mediaType == MediaType.VIDEO);
+ if (info.playable instanceof FeedMedia) {
+ FeedMedia fMedia = (FeedMedia) info.playable;
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ fMedia.getItem().getFeed().getId());
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ fMedia.getId());
+ } else {
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+ info.playable.writeToPreferences(editor);
+ } else {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+
+ editor.commit();
+ }
+
+ /**
+ * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute.
+ */
+ private void postStatusUpdateIntent() {
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ }
+
+ private void sendNotificationBroadcast(int type, int code) {
+ Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
+ intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
+ intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Used by setupNotification to load notification data in another thread.
+ */
+ private AsyncTask<Void, Void, Void> notificationSetupTask;
+
+ /**
+ * Prepares notification and starts the service in the foreground.
+ */
+ @SuppressLint("NewApi")
+ private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
+ final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ if (notificationSetupTask != null) {
+ notificationSetupTask.cancel(true);
+ }
+ notificationSetupTask = new AsyncTask<Void, Void, Void>() {
+ Bitmap icon = null;
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting background work");
+ if (android.os.Build.VERSION.SDK_INT >= 11) {
+ if (info.playable != null) {
+ try {
+ int iconSize = getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_width);
+ icon = PicassoProvider.getMediaMetadataPicassoInstance(PlaybackService.this)
+ .load(info.playable.getImageUri())
+ .resize(iconSize, iconSize)
+ .get();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+ if (icon == null) {
+ icon = BitmapFactory.decodeResource(getApplicationContext().getResources(),
+ ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()));
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ if (mediaPlayer == null) {
+ return;
+ }
+ PlaybackServiceMediaPlayer.PSMPInfo newInfo = mediaPlayer.getPSMPInfo();
+ final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
+
+ if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING
+ && info.playable != null) {
+ String contentText = info.playable.getFeedTitle();
+ String contentTitle = info.playable.getEpisodeTitle();
+ Notification notification = null;
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+ Intent pauseButtonIntent = new Intent( // pause button intent
+ PlaybackService.this, PlaybackService.class);
+ pauseButtonIntent.putExtra(
+ MediaButtonReceiver.EXTRA_KEYCODE,
+ KeyEvent.KEYCODE_MEDIA_PAUSE);
+ PendingIntent pauseButtonPendingIntent = PendingIntent
+ .getService(PlaybackService.this, 0,
+ pauseButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ Intent playButtonIntent = new Intent( // play button intent
+ PlaybackService.this, PlaybackService.class);
+ playButtonIntent.putExtra(
+ MediaButtonReceiver.EXTRA_KEYCODE,
+ KeyEvent.KEYCODE_MEDIA_PLAY);
+ PendingIntent playButtonPendingIntent = PendingIntent
+ .getService(PlaybackService.this, 1,
+ playButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ Intent stopButtonIntent = new Intent( // stop button intent
+ PlaybackService.this, PlaybackService.class);
+ stopButtonIntent.putExtra(
+ MediaButtonReceiver.EXTRA_KEYCODE,
+ KeyEvent.KEYCODE_MEDIA_STOP);
+ PendingIntent stopButtonPendingIntent = PendingIntent
+ .getService(PlaybackService.this, 2,
+ stopButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ Notification.Builder notificationBuilder = new Notification.Builder(
+ PlaybackService.this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setOngoing(true)
+ .setContentIntent(pIntent)
+ .setLargeIcon(icon)
+ .setSmallIcon(smallIcon)
+ .setPriority(UserPreferences.getNotifyPriority()) // set notification priority
+ .addAction(android.R.drawable.ic_media_play, //play action
+ getString(R.string.play_label),
+ playButtonPendingIntent)
+ .addAction(android.R.drawable.ic_media_pause, //pause action
+ getString(R.string.pause_label),
+ pauseButtonPendingIntent)
+ .addAction(android.R.drawable.ic_menu_close_clear_cancel, // stop action
+ getString(R.string.stop_label),
+ stopButtonPendingIntent);
+ notification = notificationBuilder.build();
+ } else {
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
+ PlaybackService.this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText).setOngoing(true)
+ .setContentIntent(pIntent).setLargeIcon(icon)
+ .setSmallIcon(smallIcon);
+ notification = notificationBuilder.getNotification();
+ }
+ if (newInfo.playerStatus == PlayerStatus.PLAYING) {
+ startForeground(NOTIFICATION_ID, notification);
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Notification set up");
+ }
+ }
+
+ };
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
+ notificationSetupTask
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ notificationSetupTask.execute();
+ }
+
+ }
+
+ /**
+ * Saves the current position of the media file to the DB
+ *
+ * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects
+ * @param deltaPlayedDuration value by which played_duration should be increased.
+ */
+ private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) {
+ int position = getCurrentPosition();
+ int duration = getDuration();
+ float playbackSpeed = getCurrentPlaybackSpeed();
+ final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Saving current position to " + position);
+ if (updatePlayedDuration && playable instanceof FeedMedia) {
+ FeedMedia m = (FeedMedia) playable;
+ FeedItem item = m.getItem();
+ m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed)));
+ // Auto flattr
+ if (isAutoFlattrable(m) &&
+ (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) {
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration())
+ + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration));
+ DBTasks.flattrItemIfLoggedIn(this, item);
+ }
+ }
+ playable.saveCurrentPosition(PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()),
+ position
+ );
+ }
+ }
+
+ private void stopWidgetUpdater() {
+ taskManager.cancelWidgetUpdater();
+ sendBroadcast(new Intent(STOP_WIDGET_UPDATE));
+ }
+
+ private void updateWidget() {
+ PlaybackService.this.sendBroadcast(new Intent(
+ FORCE_WIDGET_UPDATE));
+ }
+
+ public boolean sleepTimerActive() {
+ return taskManager.isSleepTimerActive();
+ }
+
+ public long getSleepTimerTimeLeft() {
+ return taskManager.getSleepTimerTimeLeft();
+ }
+
+ @SuppressLint("NewApi")
+ private RemoteControlClient setupRemoteControlClient() {
+ if (Build.VERSION.SDK_INT < 14) {
+ return null;
+ }
+
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(new ComponentName(getPackageName(),
+ MediaButtonReceiver.class.getName()));
+ PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, mediaButtonIntent, 0);
+ remoteControlClient = new RemoteControlClient(mediaPendingIntent);
+ int controlFlags;
+ if (android.os.Build.VERSION.SDK_INT < 16) {
+ controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
+ | RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
+ } else {
+ controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
+ }
+ remoteControlClient.setTransportControlFlags(controlFlags);
+ return remoteControlClient;
+ }
+
+ /**
+ * Refresh player status and metadata.
+ */
+ @SuppressLint("NewApi")
+ private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) {
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ if (remoteControlClient != null) {
+ switch (info.playerStatus) {
+ case PLAYING:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
+ break;
+ case PAUSED:
+ case INITIALIZED:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
+ break;
+ case STOPPED:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+ break;
+ case ERROR:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
+ break;
+ default:
+ remoteControlClient
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
+ }
+ if (info.playable != null) {
+ MetadataEditor editor = remoteControlClient
+ .editMetadata(false);
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
+ info.playable.getEpisodeTitle());
+
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
+ info.playable.getFeedTitle());
+
+ editor.apply();
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "RemoteControlClient state was refreshed");
+ }
+ }
+ }
+
+ private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) {
+ boolean isPlaying = false;
+
+ if (info.playerStatus == PlayerStatus.PLAYING) {
+ isPlaying = true;
+ }
+
+ if (info.playable != null) {
+ Intent i = new Intent(whatChanged);
+ i.putExtra("id", 1);
+ i.putExtra("artist", "");
+ i.putExtra("album", info.playable.getFeedTitle());
+ i.putExtra("track", info.playable.getEpisodeTitle());
+ i.putExtra("playing", isPlaying);
+ final List<FeedItem> queue = taskManager.getQueueIfLoaded();
+ if (queue != null) {
+ i.putExtra("ListSize", queue.size());
+ }
+ i.putExtra("duration", info.playable.getDuration());
+ i.putExtra("position", info.playable.getPosition());
+ sendBroadcast(i);
+ }
+ }
+
+ /**
+ * Pauses playback when the headset is disconnected and the preference is
+ * set
+ */
+ private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
+ private static final String TAG = "headsetDisconnected";
+ private static final int UNPLUGGED = 0;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) {
+ int state = intent.getIntExtra("state", -1);
+ if (state != -1) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Headset plug event. State is " + state);
+ if (state == UNPLUGGED) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Headset was unplugged during playback.");
+ pauseIfPauseOnDisconnect();
+ }
+ } else {
+ Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
+ }
+ }
+ }
+ };
+
+ private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // sound is about to change, eg. bluetooth -> speaker
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Pausing playback because audio is becoming noisy");
+ pauseIfPauseOnDisconnect();
+ }
+ // android.media.AUDIO_BECOMING_NOISY
+ };
+
+ /**
+ * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
+ */
+ private void pauseIfPauseOnDisconnect() {
+ if (UserPreferences.isPauseOnHeadsetDisconnect()) {
+ if (UserPreferences.isPersistNotify()) {
+ mediaPlayer.pause(false, true);
+ }
+ else {
+ mediaPlayer.pause(true, true);
+ }
+ }
+ }
+
+ private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ stopSelf();
+ }
+ }
+
+ };
+
+ private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
+ mediaPlayer.endPlayback();
+ }
+ }
+ };
+
+ public static MediaType getCurrentMediaType() {
+ return currentMediaType;
+ }
+
+ public void resume() {
+ mediaPlayer.resume();
+ }
+
+ public void prepare() {
+ mediaPlayer.prepare();
+ }
+
+ public void pause(boolean abandonAudioFocus, boolean reinit) {
+ mediaPlayer.pause(abandonAudioFocus, reinit);
+ }
+
+ public void reinit() {
+ mediaPlayer.reinit();
+ }
+
+ public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() {
+ return mediaPlayer.getPSMPInfo();
+ }
+
+ public PlayerStatus getStatus() {
+ return mediaPlayer.getPSMPInfo().playerStatus;
+ }
+
+ public Playable getPlayable() {
+ return mediaPlayer.getPSMPInfo().playable;
+ }
+
+ public void setSpeed(float speed) {
+ mediaPlayer.setSpeed(speed);
+ }
+
+ public boolean canSetSpeed() {
+ return mediaPlayer.canSetSpeed();
+ }
+
+ public float getCurrentPlaybackSpeed() {
+ return mediaPlayer.getPlaybackSpeed();
+ }
+
+ public boolean isStartWhenPrepared() {
+ return mediaPlayer.isStartWhenPrepared();
+ }
+
+ public void setStartWhenPrepared(boolean s) {
+ mediaPlayer.setStartWhenPrepared(s);
+ }
+
+
+ public void seekTo(final int t) {
+ mediaPlayer.seekTo(t);
+ }
+
+
+ public void seekDelta(final int d) {
+ mediaPlayer.seekDelta(d);
+ }
+
+ /**
+ * @see de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.core.feed.Chapter)
+ */
+ public void seekToChapter(Chapter c) {
+ mediaPlayer.seekToChapter(c);
+ }
+
+ /**
+ * call getDuration() on mediaplayer or return INVALID_TIME if player is in
+ * an invalid state.
+ */
+ public int getDuration() {
+ return mediaPlayer.getDuration();
+ }
+
+ /**
+ * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
+ * is in an invalid state.
+ */
+ public int getCurrentPosition() {
+ return mediaPlayer.getPosition();
+ }
+
+ public boolean isStreaming() {
+ return mediaPlayer.isStreaming();
+ }
+
+ public Pair<Integer, Integer> getVideoSize() {
+ return mediaPlayer.getVideoSize();
+ }
+
+ private boolean isAutoFlattrable(Playable p) {
+ if (p != null && p instanceof FeedMedia) {
+ FeedMedia media = (FeedMedia) p;
+ FeedItem item = ((FeedMedia) p).getItem();
+ return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred();
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
new file mode 100644
index 000000000..dbf870eac
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
@@ -0,0 +1,979 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.RemoteControlClient;
+import android.net.wifi.WifiManager;
+import android.os.PowerManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.Pair;
+import android.view.SurfaceHolder;
+
+import org.apache.commons.lang3.Validate;
+
+import java.io.IOException;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantLock;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.core.util.playback.AudioPlayer;
+import de.danoeh.antennapod.core.util.playback.IPlayer;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.VideoPlayer;
+
+/**
+ * Manages the MediaPlayer object of the PlaybackService.
+ */
+public class PlaybackServiceMediaPlayer {
+ public static final String TAG = "PlaybackServiceMediaPlayer";
+
+ /**
+ * Return value of some PSMP methods if the method call failed.
+ */
+ public static final int INVALID_TIME = -1;
+
+ private final AudioManager audioManager;
+
+ private volatile PlayerStatus playerStatus;
+ private volatile PlayerStatus statusBeforeSeeking;
+ private volatile IPlayer mediaPlayer;
+ private volatile Playable media;
+
+ private volatile boolean stream;
+ private volatile MediaType mediaType;
+ private volatile AtomicBoolean startWhenPrepared;
+ private volatile boolean pausedBecauseOfTransientAudiofocusLoss;
+ private volatile Pair<Integer, Integer> videoSize;
+
+ /**
+ * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads
+ * have to wait until these operations have finished.
+ */
+ private final ReentrantLock playerLock;
+
+ private final PSMPCallback callback;
+ private final Context context;
+
+ private final ThreadPoolExecutor executor;
+
+ /**
+ * A wifi-lock that is acquired if the media file is being streamed.
+ */
+ private WifiManager.WifiLock wifiLock;
+
+ public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) {
+ Validate.notNull(context);
+ Validate.notNull(callback);
+
+ this.context = context;
+ this.callback = callback;
+ this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ this.playerLock = new ReentrantLock();
+ this.startWhenPrepared = new AtomicBoolean(false);
+ executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(),
+ new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable");
+ }
+ }
+ );
+
+ mediaPlayer = null;
+ statusBeforeSeeking = null;
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ mediaType = MediaType.UNKNOWN;
+ playerStatus = PlayerStatus.STOPPED;
+ videoSize = null;
+ }
+
+ /**
+ * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
+ * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
+ * not do anything.
+ * Whether playback starts immediately depends on the given parameters. See below for more details.
+ * <p/>
+ * States:
+ * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
+ * <p/>
+ * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
+ * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
+ * <p/>
+ * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
+ * will enter the ERROR state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ *
+ * @param playable The Playable object that is supposed to be played. This parameter must not be null.
+ * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
+ * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
+ * the Android MediaPlayer via getStreamUrl.
+ * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the
+ * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared
+ * for playback immediately (see 'prepareImmediately' parameter for more details)
+ * @param prepareImmediately Set to true if the method should also prepare the episode for playback.
+ */
+ public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ Validate.notNull(playable);
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Play media object.");
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ try {
+ playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ throw e;
+ } finally {
+ playerLock.unlock();
+ }
+ }
+ });
+ }
+
+ /**
+ * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
+ * the given playable parameter is the same object as the currently playing media.
+ * <p/>
+ * This method requires the playerLock and is executed on the caller's thread.
+ *
+ * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean)
+ */
+ private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ Validate.notNull(playable);
+ if (!playerLock.isHeldByCurrentThread())
+ throw new IllegalStateException("method requires playerLock");
+
+
+ if (media != null) {
+ if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) {
+ // episode is already playing -> ignore method call
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
+ return;
+ } else {
+ // stop playback of this episode
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) {
+ mediaPlayer.stop();
+ }
+ setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+ }
+ }
+
+ this.media = playable;
+ this.stream = stream;
+ this.mediaType = media.getMediaType();
+ this.videoSize = null;
+ createMediaPlayer();
+ PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared);
+ setPlayerStatus(PlayerStatus.INITIALIZING, media);
+ try {
+ media.loadMetadata();
+ if (stream) {
+ mediaPlayer.setDataSource(media.getStreamUrl());
+ } else {
+ mediaPlayer.setDataSource(media.getLocalMediaUrl());
+ }
+ setPlayerStatus(PlayerStatus.INITIALIZED, media);
+
+ if (mediaType == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
+ }
+
+ if (prepareImmediately) {
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared);
+ }
+
+ } catch (Playable.PlayableException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ } catch (IOException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+
+
+ /**
+ * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
+ * nothing will happen.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void resume() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ resumeSync();
+ playerLock.unlock();
+ }
+ });
+ }
+
+ private void resumeSync() {
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
+ int focusGained = audioManager.requestAudioFocus(
+ audioFocusChangeListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ acquireWifiLockIfNecessary();
+ setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
+ mediaPlayer.start();
+ if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
+ mediaPlayer.seekTo(media.getPosition());
+ }
+
+ setPlayerStatus(PlayerStatus.PLAYING, media);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ RemoteControlClient remoteControlClient = callback.getRemoteControlClient();
+ if (remoteControlClient != null) {
+ audioManager
+ .registerRemoteControlClient(remoteControlClient);
+ }
+ }
+ audioManager
+ .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(),
+ MediaButtonReceiver.class.getName()));
+ media.onPlaybackStart();
+
+ } else {
+ if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus");
+ }
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus);
+ }
+ }
+
+
+ /**
+ * Saves the current position and pauses playback. Note that, if audiofocus
+ * is abandoned, the lockscreen controls will also disapear.
+ * <p/>
+ * This method is executed on an internal executor service.
+ *
+ * @param abandonFocus is true if the service should release audio focus
+ * @param reinit is true if service should reinit after pausing if the media
+ * file is being streamed
+ */
+ public void pause(final boolean abandonFocus, final boolean reinit) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Pausing playback.");
+ mediaPlayer.pause();
+ setPlayerStatus(PlayerStatus.PAUSED, media);
+
+ if (abandonFocus) {
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ }
+ if (stream && reinit) {
+ reinit();
+ }
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
+ }
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Prepared media player for playback if the service is in the INITALIZED
+ * state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void prepare() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.INITIALIZED) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Preparing media player");
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ try {
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared.get());
+ } catch (IOException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+ playerLock.unlock();
+
+ }
+ });
+ }
+
+ /**
+ * Called after media player has been prepared. This method is executed on the caller's thread.
+ */
+ void onPrepared(final boolean startWhenPrepared) {
+ playerLock.lock();
+
+ if (playerStatus != PlayerStatus.PREPARING) {
+ playerLock.unlock();
+ throw new IllegalStateException("Player is not in PREPARING state");
+ }
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Resource prepared");
+
+ if (mediaType == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight());
+ }
+
+ if (media.getPosition() > 0) {
+ mediaPlayer.seekTo(media.getPosition());
+ }
+
+ if (media.getDuration() == 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Setting duration of media");
+ media.setDuration(mediaPlayer.getDuration());
+ }
+ setPlayerStatus(PlayerStatus.PREPARED, media);
+
+ if (startWhenPrepared) {
+ resumeSync();
+ }
+
+ playerLock.unlock();
+ }
+
+ /**
+ * Resets the media player and moves it into INITIALIZED state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void reinit() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+ if (media != null) {
+ playMediaObject(media, true, stream, startWhenPrepared.get(), false);
+ } else if (mediaPlayer != null) {
+ mediaPlayer.reset();
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ *
+ * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0
+ * <p/>
+ * This method is executed on the caller's thread.
+ */
+ private void seekToSync(int t) {
+ if (t < 0) {
+ t = 0;
+ }
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ if (stream) {
+ // statusBeforeSeeking = playerStatus;
+ // setPlayerStatus(PlayerStatus.SEEKING, media);
+ }
+ mediaPlayer.seekTo(t);
+
+ } else if (playerStatus == PlayerStatus.INITIALIZED) {
+ media.setPosition(t);
+ startWhenPrepared.set(true);
+ prepare();
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ * Invalid time values (< 0) will be ignored.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void seekTo(final int t) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ seekToSync(t);
+ }
+ });
+ }
+
+ /**
+ * Seek a specific position from the current position
+ *
+ * @param d offset from current position (positive or negative)
+ */
+ public void seekDelta(final int d) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ int currentPosition = getPosition();
+ if (currentPosition != INVALID_TIME) {
+ seekToSync(currentPosition + d);
+ } else {
+ Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
+ }
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Seek to the start of the specified chapter.
+ */
+ public void seekToChapter(Chapter c) {
+ Validate.notNull(c);
+
+ seekTo((int) c.getStart());
+ }
+
+ /**
+ * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
+ */
+ public int getDuration() {
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getDuration();
+ } else if (media != null && media.getDuration() > 0) {
+ retVal = media.getDuration();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ /**
+ * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
+ */
+ public int getPosition() {
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getCurrentPosition();
+ } else if (media != null && media.getPosition() > 0) {
+ retVal = media.getPosition();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ public boolean isStartWhenPrepared() {
+ return startWhenPrepared.get();
+ }
+
+ public void setStartWhenPrepared(boolean startWhenPrepared) {
+ this.startWhenPrepared.set(startWhenPrepared);
+ }
+
+ /**
+ * Returns true if the playback speed can be adjusted.
+ */
+ public boolean canSetSpeed() {
+ boolean retVal = false;
+ if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
+ retVal = (mediaPlayer).canSetSpeed();
+ }
+ return retVal;
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on the caller's thread.
+ */
+ private void setSpeedSync(float speed) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ if (mediaPlayer.canSetSpeed()) {
+ mediaPlayer.setPlaybackSpeed((float) speed);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Playback speed was set to " + speed);
+ callback.playbackSpeedChanged(speed);
+ }
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on an internal executor service.
+ */
+ public void setSpeed(final float speed) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ setSpeedSync(speed);
+ }
+ });
+ }
+
+ /**
+ * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
+ */
+ public float getPlaybackSpeed() {
+ if (!playerLock.tryLock()) {
+ return 1;
+ }
+
+ float retVal = 1;
+ if ((playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) {
+ retVal = mediaPlayer.getCurrentSpeedMultiplier();
+ }
+ playerLock.unlock();
+ return retVal;
+ }
+
+ public MediaType getCurrentMediaType() {
+ return mediaType;
+ }
+
+ public boolean isStreaming() {
+ return stream;
+ }
+
+
+ /**
+ * Releases internally used resources. This method should only be called when the object is not used anymore.
+ */
+ public void shutdown() {
+ executor.shutdown();
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ releaseWifiLockIfNecessary();
+ }
+
+ public void setVideoSurface(final SurfaceHolder surface) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (mediaPlayer != null) {
+ mediaPlayer.setDisplay(surface);
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+
+ public void resetVideoSurface() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Resetting video surface");
+ mediaPlayer.setDisplay(null);
+ reinit();
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Return width and height of the currently playing video as a pair.
+ *
+ * @return Width and height as a Pair or null if the video size could not be determined. The method might still
+ * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
+ * invalid values.
+ */
+ public Pair<Integer, Integer> getVideoSize() {
+ if (!playerLock.tryLock()) {
+ // use cached value if lock can't be aquired
+ return videoSize;
+ }
+ Pair<Integer, Integer> res;
+ if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) {
+ res = null;
+ } else {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight());
+ res = videoSize;
+ }
+ playerLock.unlock();
+ return res;
+ }
+
+ /**
+ * Returns a PSMInfo object that contains information about the current state of the PSMP object.
+ *
+ * @return The PSMPInfo object.
+ */
+ public synchronized PSMPInfo getPSMPInfo() {
+ return new PSMPInfo(playerStatus, media);
+ }
+
+ /**
+ * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
+ * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
+ * <p/>
+ * This method will notify the callback about the change of the player status (even if the new status is the same
+ * as the old one).
+ *
+ * @param newStatus The new PlayerStatus. This must not be null.
+ * @param newMedia The new playable object of the PSMP object. This can be null.
+ */
+ private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) {
+ Validate.notNull(newStatus);
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus);
+
+ this.playerStatus = newStatus;
+ this.media = newMedia;
+ callback.statusChanged(new PSMPInfo(playerStatus, media));
+ }
+
+ private IPlayer createMediaPlayer() {
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ if (media == null || media.getMediaType() == MediaType.VIDEO) {
+ mediaPlayer = new VideoPlayer();
+ } else {
+ mediaPlayer = new AudioPlayer(context);
+ }
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
+ return setMediaPlayerListeners(mediaPlayer);
+ }
+
+ private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
+
+ @Override
+ public void onAudioFocusChange(final int focusChange) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ // If there is an incoming call, playback should be paused permanently
+ TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ final int callState = (tm != null) ? tm.getCallState() : 0;
+ if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState);
+ Log.i(TAG, "Call state:" + callState);
+
+ if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus");
+ pause(true, false);
+ callback.shouldStop();
+ } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Gained audio focus");
+ if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now
+ resume();
+ } else { // we ducked => raise audio level back
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_RAISE, 0);
+ }
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (!UserPreferences.shouldPauseForFocusLoss()) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Ducking...");
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_LOWER, 0);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+ };
+
+
+ public void endPlayback() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+
+ if (playerStatus != PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+ }
+ if (mediaPlayer != null) {
+ mediaPlayer.reset();
+
+ }
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ callback.endPlayback(true);
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Moves the PlaybackServiceMediaPlayer into STOPPED state. This call is only valid if the player is currently in
+ * INDETERMINATE state, for example after a call to endPlayback.
+ * This method will only take care of changing the PlayerStatus of this object! Other tasks like
+ * abandoning audio focus have to be done with other methods.
+ */
+ public void stop() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+
+ if (playerStatus == PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.STOPPED, null);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
+ }
+ playerLock.unlock();
+
+ }
+ });
+ }
+
+ private synchronized void acquireWifiLockIfNecessary() {
+ if (stream) {
+ if (wifiLock == null) {
+ wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
+ .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
+ wifiLock.setReferenceCounted(false);
+ }
+ wifiLock.acquire();
+ }
+ }
+
+ private synchronized void releaseWifiLockIfNecessary() {
+ if (wifiLock != null && wifiLock.isHeld()) {
+ wifiLock.release();
+ }
+ }
+
+ /**
+ * Holds information about a PSMP object.
+ */
+ public class PSMPInfo {
+ public PlayerStatus playerStatus;
+ public Playable playable;
+
+ public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
+ this.playerStatus = playerStatus;
+ this.playable = playable;
+ }
+ }
+
+ public static interface PSMPCallback {
+ public void statusChanged(PSMPInfo newInfo);
+
+ public void shouldStop();
+
+ public void playbackSpeedChanged(float s);
+
+ public void onBufferingUpdate(int percent);
+
+ public boolean onMediaPlayerInfo(int code);
+
+ public boolean onMediaPlayerError(Object inObj, int what, int extra);
+
+ public boolean endPlayback(boolean playNextEpisode);
+
+ public RemoteControlClient getRemoteControlClient();
+ }
+
+ private IPlayer setMediaPlayerListeners(IPlayer mp) {
+ if (mp != null && media != null) {
+ if (media.getMediaType() == MediaType.AUDIO) {
+ ((AudioPlayer) mp)
+ .setOnCompletionListener(audioCompletionListener);
+ ((AudioPlayer) mp)
+ .setOnSeekCompleteListener(audioSeekCompleteListener);
+ ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
+ ((AudioPlayer) mp)
+ .setOnBufferingUpdateListener(audioBufferingUpdateListener);
+ ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
+ } else {
+ ((VideoPlayer) mp)
+ .setOnCompletionListener(videoCompletionListener);
+ ((VideoPlayer) mp)
+ .setOnSeekCompleteListener(videoSeekCompleteListener);
+ ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
+ ((VideoPlayer) mp)
+ .setOnBufferingUpdateListener(videoBufferingUpdateListener);
+ ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
+ }
+ }
+ return mp;
+ }
+
+ private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(com.aocate.media.MediaPlayer mp) {
+ genericOnCompletion();
+ }
+ };
+
+ private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(android.media.MediaPlayer mp) {
+ genericOnCompletion();
+ }
+ };
+
+ private void genericOnCompletion() {
+ endPlayback();
+ }
+
+ private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(com.aocate.media.MediaPlayer mp,
+ int percent) {
+ genericOnBufferingUpdate(percent);
+ }
+ };
+
+ private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) {
+ genericOnBufferingUpdate(percent);
+ }
+ };
+
+ private void genericOnBufferingUpdate(int percent) {
+ callback.onBufferingUpdate(percent);
+ }
+
+ private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(com.aocate.media.MediaPlayer mp, int what,
+ int extra) {
+ return genericInfoListener(what);
+ }
+ };
+
+ private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) {
+ return genericInfoListener(what);
+ }
+ };
+
+ private boolean genericInfoListener(int what) {
+ return callback.onMediaPlayerInfo(what);
+ }
+
+ private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(com.aocate.media.MediaPlayer mp, int what,
+ int extra) {
+ return genericOnError(mp, what, extra);
+ }
+ };
+
+ private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(android.media.MediaPlayer mp, int what, int extra) {
+ return genericOnError(mp, what, extra);
+ }
+ };
+
+ private boolean genericOnError(Object inObj, int what, int extra) {
+ return callback.onMediaPlayerError(inObj, what, extra);
+ }
+
+ private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(com.aocate.media.MediaPlayer mp) {
+ genericSeekCompleteListener();
+ }
+ };
+
+ private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(android.media.MediaPlayer mp) {
+ genericSeekCompleteListener();
+ }
+ };
+
+ private final void genericSeekCompleteListener() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (playerStatus == PlayerStatus.SEEKING) {
+ setPlayerStatus(statusBeforeSeeking, media);
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
new file mode 100644
index 000000000..1865afa6f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
@@ -0,0 +1,384 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.core.BuildConfig;
+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.util.playback.Playable;
+
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * Manages the background tasks of PlaybackSerivce, i.e.
+ * the sleep timer, the position saver, the widget updater and
+ * the queue loader.
+ * <p/>
+ * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback)
+ * to notify the PlaybackService about updates from the running tasks.
+ */
+public class PlaybackServiceTaskManager {
+ private static final String TAG = "PlaybackServiceTaskManager";
+
+ /**
+ * Update interval of position saver in milliseconds.
+ */
+ public static final int POSITION_SAVER_WAITING_INTERVAL = 5000;
+ /**
+ * Notification interval of widget updater in milliseconds.
+ */
+ public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500;
+
+ private static final int SCHED_EX_POOL_SIZE = 2;
+ private final ScheduledThreadPoolExecutor schedExecutor;
+
+ private ScheduledFuture positionSaverFuture;
+ private ScheduledFuture widgetUpdaterFuture;
+ private ScheduledFuture sleepTimerFuture;
+ private volatile Future<List<FeedItem>> queueFuture;
+ private volatile Future chapterLoaderFuture;
+
+ private SleepTimer sleepTimer;
+
+ private final Context context;
+ private final PSTMCallback callback;
+
+ /**
+ * Sets up a new PSTM. This method will also start the queue loader task.
+ *
+ * @param context
+ * @param callback A PSTMCallback object for notifying the user about updates. Must not be null.
+ */
+ public PlaybackServiceTaskManager(Context context, PSTMCallback callback) {
+ Validate.notNull(context);
+ Validate.notNull(callback);
+
+ this.context = context;
+ this.callback = callback;
+ schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ });
+ loadQueue();
+ EventDistributor.getInstance().register(eventDistributorListener);
+ }
+
+ private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
+ cancelQueueLoader();
+ loadQueue();
+ }
+ }
+ };
+
+ private synchronized boolean isQueueLoaderActive() {
+ return queueFuture != null && !queueFuture.isDone();
+ }
+
+ private synchronized void cancelQueueLoader() {
+ if (isQueueLoaderActive()) {
+ queueFuture.cancel(true);
+ }
+ }
+
+ private synchronized void loadQueue() {
+ if (!isQueueLoaderActive()) {
+ queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() {
+ @Override
+ public List<FeedItem> call() throws Exception {
+ return DBReader.getQueue(context);
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns the queue if it is already loaded or null if it hasn't been loaded yet.
+ * In order to wait until the queue has been loaded, use getQueue()
+ */
+ public synchronized List<FeedItem> getQueueIfLoaded() {
+ if (queueFuture.isDone()) {
+ try {
+ return queueFuture.get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the queue or waits until the PSTM has loaded the queue from the database.
+ */
+ public synchronized List<FeedItem> getQueue() throws InterruptedException {
+ try {
+ return queueFuture.get();
+ } catch (ExecutionException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Starts the position saver task. If the position saver is already active, nothing will happen.
+ */
+ public synchronized void startPositionSaver() {
+ if (!isPositionSaverActive()) {
+ Runnable positionSaver = new Runnable() {
+ @Override
+ public void run() {
+ callback.positionSaverTick();
+ }
+ };
+ positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL,
+ POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS);
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver");
+ } else {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored.");
+ }
+ }
+
+ /**
+ * Returns true if the position saver is currently running.
+ */
+ public synchronized boolean isPositionSaverActive() {
+ return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone();
+ }
+
+ /**
+ * Cancels the position saver. If the position saver is not running, nothing will happen.
+ */
+ public synchronized void cancelPositionSaver() {
+ if (isPositionSaverActive()) {
+ positionSaverFuture.cancel(false);
+ if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver");
+ }
+ }
+
+ /**
+ * Starts the widget updater task. If the widget updater is already active, nothing will happen.
+ */
+ public synchronized void startWidgetUpdater() {
+ if (!isWidgetUpdaterActive()) {
+ Runnable widgetUpdater = new Runnable() {
+ @Override
+ public void run() {
+ callback.onWidgetUpdaterTick();
+ }
+ };
+ widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
+ WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater");
+ } else {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored.");
+ }
+ }
+
+ /**
+ * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
+ * cancelled first.
+ * After waitingTime has elapsed, onSleepTimerExpired() will be called.
+ *
+ * @throws java.lang.IllegalArgumentException if waitingTime <= 0
+ */
+ public synchronized void setSleepTimer(long waitingTime) {
+ Validate.isTrue(waitingTime > 0, "Waiting time <= 0");
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+ + " milliseconds");
+ if (isSleepTimerActive()) {
+ sleepTimerFuture.cancel(true);
+ }
+ sleepTimer = new SleepTimer(waitingTime);
+ sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Returns true if the sleep timer is currently active.
+ */
+ public synchronized boolean isSleepTimerActive() {
+ return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting;
+ }
+
+ /**
+ * Disables the sleep timer. If the sleep timer is not active, nothing will happen.
+ */
+ public synchronized void disableSleepTimer() {
+ if (isSleepTimerActive()) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Disabling sleep timer");
+ sleepTimerFuture.cancel(true);
+ }
+ }
+
+ /**
+ * Returns the current sleep timer time or 0 if the sleep timer is not active.
+ */
+ public synchronized long getSleepTimerTimeLeft() {
+ if (isSleepTimerActive()) {
+ return sleepTimer.getWaitingTime();
+ } else {
+ return 0;
+ }
+ }
+
+
+ /**
+ * Returns true if the widget updater is currently running.
+ */
+ public synchronized boolean isWidgetUpdaterActive() {
+ return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone();
+ }
+
+ /**
+ * Cancels the widget updater. If the widget updater is not running, nothing will happen.
+ */
+ public synchronized void cancelWidgetUpdater() {
+ if (isWidgetUpdaterActive()) {
+ widgetUpdaterFuture.cancel(false);
+ if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater");
+ }
+ }
+
+ private synchronized void cancelChapterLoader() {
+ if (isChapterLoaderActive()) {
+ chapterLoaderFuture.cancel(true);
+ }
+ }
+
+ private synchronized boolean isChapterLoaderActive() {
+ return chapterLoaderFuture != null && !chapterLoaderFuture.isDone();
+ }
+
+ /**
+ * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active,
+ * it will be cancelled first.
+ * On completion, the callback's onChapterLoaded method will be called.
+ */
+ public synchronized void startChapterLoader(final Playable media) {
+ Validate.notNull(media);
+
+ if (isChapterLoaderActive()) {
+ cancelChapterLoader();
+ }
+
+ Runnable chapterLoader = new Runnable() {
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Chapter loader started");
+ if (media.getChapters() == null) {
+ media.loadChapterMarks();
+ if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) {
+ callback.onChapterLoaded(media);
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Chapter loader stopped");
+ }
+ };
+ chapterLoaderFuture = schedExecutor.submit(chapterLoader);
+ }
+
+
+ /**
+ * Cancels all tasks. The PSTM will be in the initial state after execution of this method.
+ */
+ public synchronized void cancelAllTasks() {
+ cancelPositionSaver();
+ cancelWidgetUpdater();
+ disableSleepTimer();
+ cancelQueueLoader();
+ cancelChapterLoader();
+ }
+
+ /**
+ * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after
+ * execution of this method.
+ */
+ public synchronized void shutdown() {
+ EventDistributor.getInstance().unregister(eventDistributorListener);
+ cancelAllTasks();
+ schedExecutor.shutdown();
+ }
+
+ /**
+ * Sleeps for a given time and then pauses playback.
+ */
+ private class SleepTimer implements Runnable {
+ private static final String TAG = "SleepTimer";
+ private static final long UPDATE_INTERVALL = 1000L;
+ private volatile long waitingTime;
+ private volatile boolean isWaiting;
+
+ public SleepTimer(long waitingTime) {
+ super();
+ this.waitingTime = waitingTime;
+ isWaiting = true;
+ }
+
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting");
+ while (waitingTime > 0) {
+ try {
+ Thread.sleep(UPDATE_INTERVALL);
+ waitingTime -= UPDATE_INTERVALL;
+
+ if (waitingTime <= 0) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Waiting completed");
+ postExecute();
+ if (!Thread.currentThread().isInterrupted()) {
+ callback.onSleepTimerExpired();
+ }
+
+ }
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Thread was interrupted while waiting");
+ break;
+ }
+ }
+ postExecute();
+ }
+
+ protected void postExecute() {
+ isWaiting = false;
+ }
+
+ public long getWaitingTime() {
+ return waitingTime;
+ }
+
+ public boolean isWaiting() {
+ return isWaiting;
+ }
+
+ }
+
+ public static interface PSTMCallback {
+ void positionSaverTick();
+
+ void onSleepTimerExpired();
+
+ void onWidgetUpdaterTick();
+
+ void onChapterLoaded(Playable media);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
new file mode 100644
index 000000000..7c666abd5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
@@ -0,0 +1,24 @@
+package de.danoeh.antennapod.core.service.playback;
+
+public enum PlayerStatus {
+ INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state.
+ ERROR,
+ PREPARING,
+ PAUSED,
+ PLAYING,
+ STOPPED,
+ PREPARED,
+ SEEKING,
+ INITIALIZING, // playback service is loading the Playable's metadata
+ INITIALIZED; // playback service was started, data source of media player was set.
+
+ private static final PlayerStatus[] fromOrdinalLookup;
+
+ static {
+ fromOrdinalLookup = PlayerStatus.values();
+ }
+
+ public static PlayerStatus fromOrdinal(int o) {
+ return fromOrdinalLookup[o];
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
new file mode 100644
index 000000000..62edaae29
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
@@ -0,0 +1,908 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.*;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator;
+import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
+import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+import de.danoeh.antennapod.core.util.flattr.FlattrThing;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Provides methods for reading data from the AntennaPod database.
+ * In general, all database calls in DBReader-methods are executed on the caller's thread.
+ * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread.
+ * This class will use the {@link de.danoeh.antennapod.core.feed.EventDistributor} to notify listeners about changes in the database.
+ */
+public final class DBReader {
+ private static final String TAG = "DBReader";
+
+ /**
+ * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}.
+ */
+ public static final int PLAYBACK_HISTORY_SIZE = 50;
+
+ /**
+ * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}.
+ */
+ public static final int DOWNLOAD_LOG_SIZE = 200;
+
+
+ private DBReader() {
+ }
+
+ /**
+ * Returns a list of Feeds, sorted alphabetically by their title.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list of Feeds, sorted alphabetically by their title. A Feed-object
+ * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list
+ * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}.
+ */
+ public static List<Feed> getFeedList(final Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting Feedlist");
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ List<Feed> result = getFeedList(adapter);
+ adapter.close();
+ return result;
+ }
+
+ private static List<Feed> getFeedList(PodDBAdapter adapter) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting Feedlist");
+
+ Cursor feedlistCursor = adapter.getAllFeedsCursor();
+ List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount());
+
+ if (feedlistCursor.moveToFirst()) {
+ do {
+ Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor);
+ feeds.add(feed);
+ } while (feedlistCursor.moveToNext());
+ }
+ feedlistCursor.close();
+ return feeds;
+ }
+
+ /**
+ * Returns a list with the download URLs of all feeds.
+ *
+ * @param context A context that is used for opening the database connection.
+ * @return A list of Strings with the download URLs of all feeds.
+ */
+ public static List<String> getFeedListDownloadUrls(final Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ List<String> result = new ArrayList<String>();
+ adapter.open();
+ Cursor feeds = adapter.getFeedCursorDownloadUrls();
+ if (feeds.moveToFirst()) {
+ do {
+ result.add(feeds.getString(1));
+ } while (feeds.moveToNext());
+ }
+ feeds.close();
+ adapter.close();
+
+ return result;
+ }
+
+ /**
+ * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param expirationTime Time that is used for determining whether a feed is outdated or not.
+ * A Feed is considered expired if 'lastUpdate < (currentTime - expirationTime)' evaluates to true.
+ * @return A list of Feeds, sorted alphabetically by their title. A Feed-object
+ * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list
+ * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}.
+ */
+ public static List<Feed> getExpiredFeedsList(final Context context, final long expirationTime) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, String.format("getExpiredFeedsList(%d)", expirationTime));
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+
+ Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime);
+ List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount());
+
+ if (feedlistCursor.moveToFirst()) {
+ do {
+ Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor);
+ feeds.add(feed);
+ } while (feedlistCursor.moveToNext());
+ }
+ feedlistCursor.close();
+ return feeds;
+ }
+
+ /**
+ * Takes a list of FeedItems and loads their corresponding Feed-objects from the database.
+ * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will
+ * not find the correct feed of an item.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param items The FeedItems whose Feed-objects should be loaded.
+ */
+ public static void loadFeedDataOfFeedItemlist(Context context,
+ List<FeedItem> items) {
+ List<Feed> feeds = getFeedList(context);
+ for (FeedItem item : items) {
+ for (Feed feed : feeds) {
+ if (feed.getId() == item.getFeedId()) {
+ item.setFeed(feed);
+ break;
+ }
+ }
+ if (item.getFeed() == null) {
+ Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId());
+ }
+ }
+ }
+
+ /**
+ * Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not
+ * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param feed The Feed whose items should be loaded
+ * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly.
+ * The method does NOT change the items-attribute of the feed.
+ */
+ public static List<FeedItem> getFeedItemList(Context context,
+ final Feed feed) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle());
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+
+ Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed);
+ List<FeedItem> items = extractItemlistFromCursor(adapter,
+ itemlistCursor);
+ itemlistCursor.close();
+
+ Collections.sort(items, new FeedItemPubdateComparator());
+
+ adapter.close();
+
+ for (FeedItem item : items) {
+ item.setFeed(feed);
+ }
+
+ return items;
+ }
+
+ static List<FeedItem> extractItemlistFromCursor(Context context, Cursor itemlistCursor) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ List<FeedItem> result = extractItemlistFromCursor(adapter, itemlistCursor);
+ adapter.close();
+ return result;
+ }
+
+ private static List<FeedItem> extractItemlistFromCursor(
+ PodDBAdapter adapter, Cursor itemlistCursor) {
+ ArrayList<String> itemIds = new ArrayList<String>();
+ List<FeedItem> items = new ArrayList<FeedItem>(
+ itemlistCursor.getCount());
+
+ if (itemlistCursor.moveToFirst()) {
+ do {
+ FeedItem item = new FeedItem();
+
+ item.setId(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID));
+ item.setTitle(itemlistCursor
+ .getString(PodDBAdapter.IDX_FI_SMALL_TITLE));
+ item.setLink(itemlistCursor
+ .getString(PodDBAdapter.IDX_FI_SMALL_LINK));
+ item.setPubDate(new Date(itemlistCursor
+ .getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE)));
+ item.setPaymentLink(itemlistCursor
+ .getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK));
+ item.setFeedId(itemlistCursor
+ .getLong(PodDBAdapter.IDX_FI_SMALL_FEED));
+ itemIds.add(String.valueOf(item.getId()));
+
+ item.setRead((itemlistCursor
+ .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0));
+ item.setItemIdentifier(itemlistCursor
+ .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER));
+ item.setFlattrStatus(new FlattrStatus(itemlistCursor
+ .getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS)));
+
+ long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE);
+ if (imageIndex != 0) {
+ item.setImage(getFeedImage(adapter, imageIndex));
+ }
+
+ // extract chapters
+ boolean hasSimpleChapters = itemlistCursor
+ .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0;
+ if (hasSimpleChapters) {
+ Cursor chapterCursor = adapter
+ .getSimpleChaptersOfFeedItemCursor(item);
+ if (chapterCursor.moveToFirst()) {
+ item.setChapters(new ArrayList<Chapter>());
+ do {
+ int chapterType = chapterCursor
+ .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX);
+ Chapter chapter = null;
+ long start = chapterCursor
+ .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX);
+ String title = chapterCursor
+ .getString(PodDBAdapter.KEY_TITLE_INDEX);
+ String link = chapterCursor
+ .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX);
+
+ switch (chapterType) {
+ case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
+ chapter = new SimpleChapter(start, title, item,
+ link);
+ break;
+ case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
+ chapter = new ID3Chapter(start, title, item,
+ link);
+ break;
+ case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
+ chapter = new VorbisCommentChapter(start,
+ title, item, link);
+ break;
+ }
+ if (chapter != null) {
+ chapter.setId(chapterCursor
+ .getLong(PodDBAdapter.KEY_ID_INDEX));
+ item.getChapters().add(chapter);
+ }
+ } while (chapterCursor.moveToNext());
+ }
+ chapterCursor.close();
+ }
+ items.add(item);
+ } while (itemlistCursor.moveToNext());
+ }
+
+ extractMediafromItemlist(adapter, items, itemIds);
+ return items;
+ }
+
+ private static void extractMediafromItemlist(PodDBAdapter adapter,
+ List<FeedItem> items, ArrayList<String> itemIds) {
+
+ List<FeedItem> itemsCopy = new ArrayList<FeedItem>(items);
+ Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds
+ .toArray(new String[itemIds.size()]));
+ if (cursor.moveToFirst()) {
+ do {
+ long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX);
+ // find matching feed item
+ FeedItem item = getMatchingItemForMedia(itemId, itemsCopy);
+ if (item != null) {
+ item.setMedia(extractFeedMediaFromCursorRow(cursor));
+ item.getMedia().setItem(item);
+ }
+ } while (cursor.moveToNext());
+ cursor.close();
+ }
+ }
+
+ private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) {
+ long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
+ Date playbackCompletionDate = null;
+ long playbackCompletionTime = cursor
+ .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX);
+ if (playbackCompletionTime > 0) {
+ playbackCompletionDate = new Date(
+ playbackCompletionTime);
+ }
+
+ return new FeedMedia(
+ mediaId,
+ null,
+ cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX),
+ cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX),
+ cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX),
+ cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX),
+ cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX),
+ cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX),
+ cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0,
+ playbackCompletionDate,
+ cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX));
+ }
+
+ private static Feed extractFeedFromCursorRow(PodDBAdapter adapter,
+ Cursor cursor) {
+ Date lastUpdate = new Date(
+ cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_LASTUPDATE));
+
+ final FeedImage image;
+ long imageIndex = cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_IMAGE);
+ if (imageIndex != 0) {
+ image = getFeedImage(adapter, imageIndex);
+ } else {
+ image = null;
+ }
+ Feed feed = new Feed(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID),
+ lastUpdate,
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TITLE),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LINK),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DESCRIPTION),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_PAYMENT_LINK),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_AUTHOR),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LANGUAGE),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TYPE),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FEED_IDENTIFIER),
+ image,
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL),
+ cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0,
+ new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS)));
+
+ if (image != null) {
+ image.setOwner(feed);
+ }
+
+ FeedPreferences preferences = new FeedPreferences(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID),
+ cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0,
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_USERNAME),
+ cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_PASSWORD));
+
+ feed.setPreferences(preferences);
+ return feed;
+ }
+
+ private static FeedItem getMatchingItemForMedia(long itemId,
+ List<FeedItem> items) {
+ for (FeedItem item : items) {
+ if (item.getId() == itemId) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting queue");
+
+ Cursor itemlistCursor = adapter.getQueueCursor();
+ List<FeedItem> items = extractItemlistFromCursor(adapter,
+ itemlistCursor);
+ itemlistCursor.close();
+ loadFeedDataOfFeedItemlist(context, items);
+
+ return items;
+ }
+
+ /**
+ * Loads the IDs of the FeedItems in the queue. This method should be preferred over
+ * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned
+ * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties.
+ */
+ public static List<Long> getQueueIDList(Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+
+ adapter.open();
+ List<Long> result = getQueueIDList(adapter);
+ adapter.close();
+
+ return result;
+ }
+
+ static List<Long> getQueueIDList(PodDBAdapter adapter) {
+ adapter.open();
+ Cursor queueCursor = adapter.getQueueIDCursor();
+
+ List<Long> queueIds = new ArrayList<Long>(queueCursor.getCount());
+ if (queueCursor.moveToFirst()) {
+ do {
+ queueIds.add(queueCursor.getLong(0));
+ } while (queueCursor.moveToNext());
+ }
+ return queueIds;
+ }
+
+
+ /**
+ * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using
+ * {@link #getQueueIDList(android.content.Context)} instead.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned
+ * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties.
+ */
+ public static List<FeedItem> getQueue(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting queue");
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ List<FeedItem> items = getQueue(context, adapter);
+ adapter.close();
+ return items;
+ }
+
+ /**
+ * Loads a list of FeedItems whose episode has been downloaded.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list of FeedItems whose episdoe has been downloaded.
+ */
+ public static List<FeedItem> getDownloadedItems(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting downloaded items");
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+
+ Cursor itemlistCursor = adapter.getDownloadedItemsCursor();
+ List<FeedItem> items = extractItemlistFromCursor(adapter,
+ itemlistCursor);
+ itemlistCursor.close();
+ loadFeedDataOfFeedItemlist(context, items);
+ Collections.sort(items, new FeedItemPubdateComparator());
+
+ adapter.close();
+ return items;
+
+ }
+
+ /**
+ * Loads a list of FeedItems whose 'read'-attribute is set to false.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used,
+ * consider using {@link #getUnreadItemIds(android.content.Context)} instead.
+ */
+ public static List<FeedItem> getUnreadItemsList(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting unread items list");
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+
+ Cursor itemlistCursor = adapter.getUnreadItemsCursor();
+ List<FeedItem> items = extractItemlistFromCursor(adapter,
+ itemlistCursor);
+ itemlistCursor.close();
+
+ loadFeedDataOfFeedItemlist(context, items);
+
+ adapter.close();
+
+ return items;
+ }
+
+ /**
+ * Loads the IDs of the FeedItems whose 'read'-attribute is set to false.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred
+ * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used.
+ */
+ public static long[] getUnreadItemIds(Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor cursor = adapter.getUnreadItemIdsCursor();
+ long[] itemIds = new long[cursor.getCount()];
+ int i = 0;
+ if (cursor.moveToFirst()) {
+ do {
+ itemIds[i] = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
+ i++;
+ } while (cursor.moveToNext());
+ }
+ return itemIds;
+ }
+
+
+ /**
+ * Loads a list of FeedItems sorted by pubDate in descending order.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param limit The maximum number of episodes that should be loaded.
+ */
+ public static List<FeedItem> getRecentlyPublishedEpisodes(Context context, int limit) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting recently published items list");
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+
+ Cursor itemlistCursor = adapter.getRecentlyPublishedItemsCursor(limit);
+ List<FeedItem> items = extractItemlistFromCursor(adapter,
+ itemlistCursor);
+ itemlistCursor.close();
+
+ loadFeedDataOfFeedItemlist(context, items);
+
+ adapter.close();
+
+ return items;
+ }
+
+ /**
+ * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode
+ * has been completed at least once.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order.
+ * The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}.
+ */
+ public static List<FeedItem> getPlaybackHistory(final Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading playback history");
+ final int PLAYBACK_HISTORY_SIZE = 50;
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+
+ Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE);
+ String[] itemIds = new String[mediaCursor.getCount()];
+ for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) {
+ itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX));
+ }
+ mediaCursor.close();
+ Cursor itemCursor = adapter.getFeedItemCursor(itemIds);
+ List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor);
+ loadFeedDataOfFeedItemlist(context, items);
+ itemCursor.close();
+ adapter.close();
+
+ Collections.sort(items, new PlaybackCompletionDateComparator());
+ return items;
+ }
+
+ /**
+ * Loads the download log from the database.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list with DownloadStatus objects that represent the download log.
+ * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}.
+ */
+ public static List<DownloadStatus> getDownloadLog(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Extracting DownloadLog");
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE);
+ List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>(
+ logCursor.getCount());
+
+ if (logCursor.moveToFirst()) {
+ do {
+ long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
+
+ long feedfileId = logCursor
+ .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX);
+ int feedfileType = logCursor
+ .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX);
+ boolean successful = logCursor
+ .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0;
+ int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX);
+ String reasonDetailed = logCursor
+ .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX);
+ String title = logCursor
+ .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX);
+ Date completionDate = new Date(
+ logCursor
+ .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)
+ );
+ downloadLog.add(new DownloadStatus(id, title, feedfileId,
+ feedfileType, successful, DownloadError.fromCode(reason), completionDate,
+ reasonDetailed));
+
+ } while (logCursor.moveToNext());
+ }
+ logCursor.close();
+ Collections.sort(downloadLog, new DownloadStatusComparator());
+ return downloadLog;
+ }
+
+ /**
+ * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over
+ * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about
+ * the FeedItems is needed.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title.
+ */
+ public static List<FeedItemStatistics> getFeedStatisticsList(final Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ List<FeedItemStatistics> result = new ArrayList<FeedItemStatistics>();
+ Cursor cursor = adapter.getFeedStatisticsCursor();
+ if (cursor.moveToFirst()) {
+ do {
+ result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED),
+ cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS),
+ cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS),
+ cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES),
+ new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE))));
+ } while (cursor.moveToNext());
+ }
+
+ cursor.close();
+ adapter.close();
+ return result;
+ }
+
+ /**
+ * Loads a specific Feed from the database.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param feedId The ID of the Feed
+ * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
+ * database and the items-attribute will be set correctly.
+ */
+ public static Feed getFeed(final Context context, final long feedId) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Feed result = getFeed(context, feedId, adapter);
+ adapter.close();
+ return result;
+ }
+
+ static Feed getFeed(final Context context, final long feedId, PodDBAdapter adapter) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading feed with id " + feedId);
+ Feed feed = null;
+
+ Cursor feedCursor = adapter.getFeedCursor(feedId);
+ if (feedCursor.moveToFirst()) {
+ feed = extractFeedFromCursorRow(adapter, feedCursor);
+ feed.setItems(getFeedItemList(context, feed));
+ } else {
+ Log.e(TAG, "getFeed could not find feed with id " + feedId);
+ }
+ feedCursor.close();
+ return feed;
+ }
+
+ static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading feeditem with id " + itemId);
+ FeedItem item = null;
+
+ Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId));
+ if (itemCursor.moveToFirst()) {
+ List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor);
+ if (list.size() > 0) {
+ item = list.get(0);
+ loadFeedDataOfFeedItemlist(context, list);
+ }
+ }
+ return item;
+
+ }
+
+ /**
+ * Loads a specific FeedItem from the database.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param itemId The ID of the FeedItem
+ * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes of the FeedItem will
+ * also be loaded from the database.
+ */
+ public static FeedItem getFeedItem(final Context context, final long itemId) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Loading feeditem with id " + itemId);
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ FeedItem item = getFeedItem(context, itemId, adapter);
+ adapter.close();
+ return item;
+
+ }
+
+ /**
+ * Loads additional information about a FeedItem, e.g. shownotes
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param item The FeedItem
+ */
+ public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor extraCursor = adapter.getExtraInformationOfItem(item);
+ if (extraCursor.moveToFirst()) {
+ String description = extraCursor
+ .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION);
+ String contentEncoded = extraCursor
+ .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED);
+ item.setDescription(description);
+ item.setContentEncoded(contentEncoded);
+ }
+ adapter.close();
+ }
+
+ /**
+ * Returns the number of downloaded episodes.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return The number of downloaded episodes.
+ */
+ public static int getNumberOfDownloadedEpisodes(final Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ final int result = adapter.getNumberOfDownloadedEpisodes();
+ adapter.close();
+ return result;
+ }
+
+ /**
+ * Returns the number of unread items.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return The number of unread items.
+ */
+ public static int getNumberOfUnreadItems(final Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ final int result = adapter.getNumberOfUnreadItems();
+ adapter.close();
+ return result;
+ }
+
+ /**
+ * Searches the DB for a FeedImage of the given id.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param imageId The id of the object
+ * @return The found object
+ */
+ public static FeedImage getFeedImage(final Context context, final long imageId) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ FeedImage result = getFeedImage(adapter, imageId);
+ adapter.close();
+ return result;
+ }
+
+ /**
+ * Searches the DB for a FeedImage of the given id.
+ *
+ * @param id The id of the object
+ * @return The found object
+ */
+ static FeedImage getFeedImage(PodDBAdapter adapter, final long id) {
+ Cursor cursor = adapter.getImageCursor(id);
+ if ((cursor.getCount() == 0) || !cursor.moveToFirst()) {
+ throw new SQLException("No FeedImage found at index: " + id);
+ }
+ FeedImage image = new FeedImage(id, cursor.getString(cursor
+ .getColumnIndex(PodDBAdapter.KEY_TITLE)),
+ cursor.getString(cursor
+ .getColumnIndex(PodDBAdapter.KEY_FILE_URL)),
+ cursor.getString(cursor
+ .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)),
+ cursor.getInt(cursor
+ .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0
+ );
+ cursor.close();
+ return image;
+ }
+
+ /**
+ * Searches the DB for a FeedMedia of the given id.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param mediaId The id of the object
+ * @return The found object
+ */
+ public static FeedMedia getFeedMedia(final Context context, final long mediaId) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+
+ adapter.open();
+ Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId);
+
+ FeedMedia media = null;
+ if (mediaCursor.moveToFirst()) {
+ final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX);
+ media = extractFeedMediaFromCursorRow(mediaCursor);
+ FeedItem item = getFeedItem(context, itemId);
+ if (media != null && item != null) {
+ media.setItem(item);
+ item.setMedia(media);
+ }
+ }
+
+ mediaCursor.close();
+ adapter.close();
+
+ return media;
+ }
+
+ /**
+ * Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @return The flattr queue as a List.
+ */
+ public static List<FlattrThing> getFlattrQueue(Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ List<FlattrThing> result = new ArrayList<FlattrThing>();
+
+ // load feeds
+ Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor();
+ if (feedCursor.moveToFirst()) {
+ do {
+ result.add(extractFeedFromCursorRow(adapter, feedCursor));
+ } while (feedCursor.moveToNext());
+ }
+ feedCursor.close();
+
+ //load feed items
+ Cursor feedItemCursor = adapter.getFeedItemsInFlattrQueueCursor();
+ result.addAll(extractItemlistFromCursor(adapter, feedItemCursor));
+ feedItemCursor.close();
+
+ adapter.close();
+ Log.d(TAG, "Returning flattrQueueIterator for queue with " + result.size() + " items.");
+ return result;
+ }
+
+
+ /**
+ * Returns true if the flattr queue is empty.
+ *
+ * @param context A context that is used for opening a database connection.
+ */
+ public static boolean getFlattrQueueEmpty(Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ boolean empty = adapter.getFlattrQueueSize() == 0;
+ adapter.close();
+ return empty;
+ }
+
+ /**
+ * Returns data necessary for displaying the navigation drawer. This includes
+ * the list of subscriptions, the number of items in the queue and the number of unread
+ * items.
+ *
+ * @param context A context that is used for opening a database connection.
+ */
+ public static NavDrawerData getNavDrawerData(Context context) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ List<Feed> feeds = getFeedList(adapter);
+ int queueSize = adapter.getQueueSize();
+ int numUnreadItems = adapter.getNumberOfUnreadItems();
+ NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems);
+ adapter.close();
+ return result;
+ }
+
+ public static class NavDrawerData {
+ public List<Feed> feeds;
+ public int queueSize;
+ public int numUnreadItems;
+
+ public NavDrawerData(List<Feed> feeds, int queueSize, int numUnreadItems) {
+ this.feeds = feeds;
+ this.queueSize = queueSize;
+ this.numUnreadItems = numUnreadItems;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
new file mode 100644
index 000000000..cc0a3d058
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
@@ -0,0 +1,898 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.asynctask.FlattrClickWorker;
+import de.danoeh.antennapod.core.asynctask.FlattrStatusFetcher;
+import de.danoeh.antennapod.core.feed.*;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.GpodnetSyncService;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
+import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Provides methods for doing common tasks that use DBReader and DBWriter.
+ */
+public final class DBTasks {
+ private static final String TAG = "DBTasks";
+
+ /**
+ * Executor service used by the autodownloadUndownloadedEpisodes method.
+ */
+ private static ExecutorService autodownloadExec;
+
+ static {
+ autodownloadExec = Executors.newSingleThreadExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ });
+ }
+
+ private DBTasks() {
+ }
+
+ /**
+ * Removes the feed with the given download url. This method should NOT be executed on the GUI thread.
+ *
+ * @param context Used for accessing the db
+ * @param downloadUrl URL of the feed.
+ */
+ public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor cursor = adapter.getFeedCursorDownloadUrls();
+ long feedID = 0;
+ if (cursor.moveToFirst()) {
+ do {
+ if (cursor.getString(1).equals(downloadUrl)) {
+ feedID = cursor.getLong(0);
+ }
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ adapter.close();
+
+ if (feedID != 0) {
+ try {
+ DBWriter.deleteFeed(context, feedID).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ } else {
+ Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl);
+ }
+ }
+
+ /**
+ * Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to
+ * start the {@link PlaybackService}.
+ *
+ * @param context Used for sending starting Services and Activities.
+ * @param media The FeedMedia object.
+ * @param showPlayer If true, starts the appropriate player activity ({@link de.danoeh.antennapod.activity.AudioplayerActivity}
+ * or {@link de.danoeh.antennapod.activity.VideoplayerActivity}
+ * @param startWhenPrepared Parameter for the {@link PlaybackService} start intent. If true, playback will start as
+ * soon as the PlaybackService has finished loading the FeedMedia object's file.
+ * @param shouldStream Parameter for the {@link PlaybackService} start intent. If true, the FeedMedia object's file
+ * will be streamed, otherwise the downloaded file will be used. If the downloaded file cannot be
+ * found, the PlaybackService will shutdown and the database entry of the FeedMedia object will be
+ * corrected.
+ */
+ public static void playMedia(final Context context, final FeedMedia media,
+ boolean showPlayer, boolean startWhenPrepared, boolean shouldStream) {
+ try {
+ if (!shouldStream) {
+ if (media.fileExists() == false) {
+ throw new MediaFileNotFoundException(
+ "No episode was found at " + media.getFile_url(),
+ media);
+ }
+ }
+ // Start playback Service
+ Intent launchIntent = new Intent(context, PlaybackService.class);
+ launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
+ launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
+ startWhenPrepared);
+ launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
+ shouldStream);
+ launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
+ true);
+ context.startService(launchIntent);
+ if (showPlayer) {
+ // Launch media player
+ context.startActivity(PlaybackService.getPlayerActivityIntent(
+ context, media));
+ }
+ DBWriter.addQueueItemAt(context, media.getItem().getId(), 0, false);
+ } catch (MediaFileNotFoundException e) {
+ e.printStackTrace();
+ if (media.isPlaying()) {
+ context.sendBroadcast(new Intent(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ }
+ notifyMissingFeedMediaFile(context, media);
+ }
+ }
+
+ private static AtomicBoolean isRefreshing = new AtomicBoolean(false);
+
+ /**
+ * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still
+ * enqueuing Feeds for download from a previous call
+ *
+ * @param context Might be used for accessing the database
+ * @param feeds List of Feeds that should be refreshed.
+ */
+ public static void refreshAllFeeds(final Context context,
+ final List<Feed> feeds) {
+ if (isRefreshing.compareAndSet(false, true)) {
+ new Thread() {
+ public void run() {
+ if (feeds != null) {
+ refreshFeeds(context, feeds);
+ } else {
+ refreshFeeds(context, DBReader.getFeedList(context));
+ }
+ isRefreshing.set(false);
+
+ if (FlattrUtils.hasToken()) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Flattring all pending things.");
+ new FlattrClickWorker(context).executeAsync(); // flattr pending things
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Fetching flattr status.");
+ new FlattrStatusFetcher(context).start();
+
+ }
+ if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
+ GpodnetSyncService.sendSyncIntent(context);
+ }
+ autodownloadUndownloadedItems(context);
+ }
+ }.start();
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Ignoring request to refresh all feeds: Refresh lock is locked");
+ }
+ }
+
+ /**
+ * Used by refreshExpiredFeeds to determine which feeds should be refreshed.
+ * This method will use the value specified in the UserPreferences as the
+ * expiration time.
+ *
+ * @param context Used for DB access.
+ * @return A list of expired feeds. An empty list will be returned if there
+ * are no expired feeds.
+ */
+ public static List<Feed> getExpiredFeeds(final Context context) {
+ long millis = UserPreferences.getUpdateInterval();
+
+ if (millis > 0) {
+
+ List<Feed> feedList = DBReader.getExpiredFeedsList(context,
+ millis);
+ if (feedList.size() > 0) {
+ refreshFeeds(context, feedList);
+ }
+ return feedList;
+ } else {
+ return new ArrayList<Feed>();
+ }
+ }
+
+ /**
+ * Refreshes expired Feeds in the list returned by the getExpiredFeedsList(Context, long) method in DBReader.
+ * The expiration date parameter is determined by the update interval specified in {@link UserPreferences}.
+ *
+ * @param context Used for DB access.
+ */
+ public static void refreshExpiredFeeds(final Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Refreshing expired feeds");
+
+ new Thread() {
+ public void run() {
+ refreshFeeds(context, getExpiredFeeds(context));
+ }
+ }.start();
+ }
+
+ private static void refreshFeeds(final Context context,
+ final List<Feed> feedList) {
+
+ for (Feed feed : feedList) {
+ try {
+ refreshFeed(context, feed);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DBWriter.addDownloadStatus(
+ context,
+ new DownloadStatus(feed, feed
+ .getHumanReadableIdentifier(),
+ DownloadError.ERROR_REQUEST_ERROR, false, e
+ .getMessage()
+ )
+ );
+ }
+ }
+
+ }
+
+ /**
+ * Updates a specific Feed.
+ *
+ * @param context Used for requesting the download.
+ * @param feed The Feed object.
+ */
+ public static void refreshFeed(Context context, Feed feed)
+ throws DownloadRequestException {
+ Feed f;
+ if (feed.getPreferences() == null) {
+ f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle());
+ } else {
+ f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle(),
+ feed.getPreferences().getUsername(), feed.getPreferences().getPassword());
+ }
+ f.setId(feed.getId());
+ DownloadRequester.getInstance().downloadFeed(context, f);
+ }
+
+ /**
+ * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file.
+ *
+ * @param context Used for requesting the download.
+ * @param image The FeedImage object.
+ */
+ public static void notifyInvalidImageFile(final Context context,
+ final FeedImage image) {
+ Log.i(TAG,
+ "The DB was notified about an invalid image download. It will now try to re-download the image file");
+ try {
+ DownloadRequester.getInstance().downloadImage(context, image);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ Log.w(TAG, "Failed to download invalid feed image");
+ }
+ }
+
+ /**
+ * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the
+ * DB and send a FeedUpdateBroadcast.
+ */
+ public static void notifyMissingFeedMediaFile(final Context context,
+ final FeedMedia media) {
+ Log.i(TAG,
+ "The feedmanager was notified about a missing episode. It will update its database now.");
+ media.setDownloaded(false);
+ media.setFile_url(null);
+ DBWriter.setFeedMedia(context, media);
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
+ }
+
+ /**
+ * Request the download of all objects in the queue. from a separate Thread.
+ *
+ * @param context Used for requesting the download an accessing the database.
+ */
+ public static void downloadAllItemsInQueue(final Context context) {
+ new Thread() {
+ public void run() {
+ List<FeedItem> queue = DBReader.getQueue(context);
+ if (!queue.isEmpty()) {
+ try {
+ downloadFeedItems(context,
+ queue.toArray(new FeedItem[queue.size()]));
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * Requests the download of a list of FeedItem objects.
+ *
+ * @param context Used for requesting the download and accessing the DB.
+ * @param items The FeedItem objects.
+ */
+ public static void downloadFeedItems(final Context context,
+ FeedItem... items) throws DownloadRequestException {
+ downloadFeedItems(true, context, items);
+ }
+
+ private static void downloadFeedItems(boolean performAutoCleanup,
+ final Context context, final FeedItem... items)
+ throws DownloadRequestException {
+ final DownloadRequester requester = DownloadRequester.getInstance();
+
+ if (performAutoCleanup) {
+ new Thread() {
+
+ @Override
+ public void run() {
+ performAutoCleanup(context,
+ getPerformAutoCleanupArgs(context, items.length));
+ }
+
+ }.start();
+ }
+ for (FeedItem item : items) {
+ if (item.getMedia() != null
+ && !requester.isDownloadingFile(item.getMedia())
+ && !item.getMedia().isDownloaded()) {
+ if (items.length > 1) {
+ try {
+ requester.downloadMedia(context, item.getMedia());
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DBWriter.addDownloadStatus(context,
+ new DownloadStatus(item.getMedia(), item
+ .getMedia()
+ .getHumanReadableIdentifier(),
+ DownloadError.ERROR_REQUEST_ERROR,
+ false, e.getMessage()
+ )
+ );
+ }
+ } else {
+ requester.downloadMedia(context, item.getMedia());
+ }
+ }
+ }
+ }
+
+ private static int getNumberOfUndownloadedEpisodes(
+ final List<FeedItem> queue, final List<FeedItem> unreadItems) {
+ int counter = 0;
+ for (FeedItem item : queue) {
+ if (item.hasMedia() && !item.getMedia().isDownloaded()
+ && !item.getMedia().isPlaying()
+ && item.getFeed().getPreferences().getAutoDownload()) {
+ counter++;
+ }
+ }
+ for (FeedItem item : unreadItems) {
+ if (item.hasMedia() && !item.getMedia().isDownloaded()
+ && item.getFeed().getPreferences().getAutoDownload()) {
+ counter++;
+ }
+ }
+ return counter;
+ }
+
+ /**
+ * Looks for undownloaded episodes in the queue or list of unread items and request a download if
+ * 1. Network is available
+ * 2. There is free space in the episode cache
+ * This method is executed on an internal single thread executor.
+ *
+ * @param context Used for accessing the DB.
+ * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if
+ * its media ID is in the mediaIds list.
+ * @return A Future that can be used for waiting for the methods completion.
+ */
+ public static Future<?> autodownloadUndownloadedItems(final Context context, final long... mediaIds) {
+ return autodownloadExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Performing auto-dl of undownloaded episodes");
+ if (NetworkUtils.autodownloadNetworkAvailable(context)
+ && UserPreferences.isEnableAutodownload()) {
+ final List<FeedItem> queue = DBReader.getQueue(context);
+ final List<FeedItem> unreadItems = DBReader
+ .getUnreadItemsList(context);
+
+ int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue,
+ unreadItems);
+ int downloadedEpisodes = DBReader
+ .getNumberOfDownloadedEpisodes(context);
+ int deletedEpisodes = performAutoCleanup(context,
+ getPerformAutoCleanupArgs(context, undownloadedEpisodes));
+ int episodeSpaceLeft = undownloadedEpisodes;
+ boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences
+ .getEpisodeCacheSizeUnlimited();
+
+ if (!cacheIsUnlimited
+ && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes
+ + undownloadedEpisodes) {
+ episodeSpaceLeft = UserPreferences.getEpisodeCacheSize()
+ - (downloadedEpisodes - deletedEpisodes);
+ }
+
+ Arrays.sort(mediaIds); // sort for binary search
+ final boolean ignoreMediaIds = mediaIds.length == 0;
+ List<FeedItem> itemsToDownload = new ArrayList<FeedItem>();
+
+ if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
+ for (int i = 0; i < queue.size(); i++) { // ignore playing item
+ FeedItem item = queue.get(i);
+ long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
+ if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
+ && item.hasMedia()
+ && !item.getMedia().isDownloaded()
+ && !item.getMedia().isPlaying()
+ && item.getFeed().getPreferences().getAutoDownload()) {
+ itemsToDownload.add(item);
+ episodeSpaceLeft--;
+ undownloadedEpisodes--;
+ if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
+ for (FeedItem item : unreadItems) {
+ long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
+ if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
+ && item.hasMedia()
+ && !item.getMedia().isDownloaded()
+ && item.getFeed().getPreferences().getAutoDownload()) {
+ itemsToDownload.add(item);
+ episodeSpaceLeft--;
+ undownloadedEpisodes--;
+ if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
+ break;
+ }
+ }
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Enqueueing " + itemsToDownload.size()
+ + " items for download");
+
+ try {
+ downloadFeedItems(false, context,
+ itemsToDownload.toArray(new FeedItem[itemsToDownload
+ .size()])
+ );
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ }
+
+ }
+ }
+ });
+
+ }
+
+ private static int getPerformAutoCleanupArgs(Context context,
+ final int episodeNumber) {
+ if (episodeNumber >= 0
+ && UserPreferences.getEpisodeCacheSize() != UserPreferences
+ .getEpisodeCacheSizeUnlimited()) {
+ int downloadedEpisodes = DBReader
+ .getNumberOfDownloadedEpisodes(context);
+ if (downloadedEpisodes + episodeNumber >= UserPreferences
+ .getEpisodeCacheSize()) {
+
+ return downloadedEpisodes + episodeNumber
+ - UserPreferences.getEpisodeCacheSize();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller
+ * 'playbackCompletionDate'-value will be deleted first.
+ * <p/>
+ * This method should NOT be executed on the GUI thread.
+ *
+ * @param context Used for accessing the DB.
+ */
+ public static void performAutoCleanup(final Context context) {
+ performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0));
+ }
+
+ private static int performAutoCleanup(final Context context,
+ final int episodeNumber) {
+ List<FeedItem> candidates = new ArrayList<FeedItem>();
+ List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context);
+ QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context));
+ List<FeedItem> delete;
+ for (FeedItem item : downloadedItems) {
+ if (item.hasMedia() && item.getMedia().isDownloaded()
+ && !queue.contains(item.getId()) && item.isRead()) {
+ candidates.add(item);
+ }
+
+ }
+
+ Collections.sort(candidates, new Comparator<FeedItem>() {
+ @Override
+ public int compare(FeedItem lhs, FeedItem rhs) {
+ Date l = lhs.getMedia().getPlaybackCompletionDate();
+ Date r = rhs.getMedia().getPlaybackCompletionDate();
+
+ if (l == null) {
+ l = new Date(0);
+ }
+ if (r == null) {
+ r = new Date(0);
+ }
+ return l.compareTo(r);
+ }
+ });
+
+ if (candidates.size() > episodeNumber) {
+ delete = candidates.subList(0, episodeNumber);
+ } else {
+ delete = candidates;
+ }
+
+ for (FeedItem item : delete) {
+ try {
+ DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int counter = delete.size();
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, String.format(
+ "Auto-delete deleted %d episodes (%d requested)", counter,
+ episodeNumber));
+
+ return counter;
+ }
+
+ /**
+ * Adds all FeedItem objects whose 'read'-attribute is false to the queue in a separate thread.
+ */
+ public static void enqueueAllNewItems(final Context context) {
+ long[] unreadItems = DBReader.getUnreadItemIds(context);
+ DBWriter.addQueueItem(context, unreadItems);
+ }
+
+ /**
+ * Returns the successor of a FeedItem in the queue.
+ *
+ * @param context Used for accessing the DB.
+ * @param itemId ID of the FeedItem
+ * @param queue Used for determining the successor of the item. If this parameter is null, the method will load
+ * the queue from the database in the same thread.
+ * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor.
+ */
+ public static FeedItem getQueueSuccessorOfItem(Context context,
+ final long itemId, List<FeedItem> queue) {
+ FeedItem result = null;
+ if (queue == null) {
+ queue = DBReader.getQueue(context);
+ }
+ if (queue != null) {
+ Iterator<FeedItem> iterator = queue.iterator();
+ while (iterator.hasNext()) {
+ FeedItem item = iterator.next();
+ if (item.getId() == itemId) {
+ if (iterator.hasNext()) {
+ result = iterator.next();
+ }
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Loads the queue from the database and checks if the specified FeedItem is in the queue.
+ * This method should NOT be executed in the GUI thread.
+ *
+ * @param context Used for accessing the DB.
+ * @param feedItemId ID of the FeedItem
+ */
+ public static boolean isInQueue(Context context, final long feedItemId) {
+ List<Long> queue = DBReader.getQueueIDList(context);
+ return QueueAccess.IDListAccess(queue).contains(feedItemId);
+ }
+
+ private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter,
+ Feed feed) {
+ if (feed.getId() != 0) {
+ return DBReader.getFeed(context, feed.getId(), adapter);
+ } else {
+ List<Feed> feeds = DBReader.getFeedList(context);
+ for (Feed f : feeds) {
+ if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) {
+ f.setItems(DBReader.getFeedItemList(context, f));
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get a FeedItem by its identifying value.
+ */
+ private static FeedItem searchFeedItemByIdentifyingValue(Feed feed,
+ String identifier) {
+ for (FeedItem item : feed.getItems()) {
+ if (item.getIdentifyingValue().equals(identifier)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same
+ * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed.
+ * These FeedItems will be marked as unread.
+ * <p/>
+ * This method can update multiple feeds at once. Submitting a feed twice in the same method call can result in undefined behavior.
+ * <p/>
+ * This method should NOT be executed on the GUI thread.
+ *
+ * @param context Used for accessing the DB.
+ * @param newFeeds The new Feed objects.
+ * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise.
+ */
+ public static synchronized Feed[] updateFeed(final Context context,
+ final Feed... newFeeds) {
+ List<Feed> newFeedsList = new ArrayList<Feed>();
+ List<Feed> updatedFeedsList = new ArrayList<Feed>();
+ Feed[] resultFeeds = new Feed[newFeeds.length];
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+
+ for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) {
+
+ final Feed newFeed = newFeeds[feedIdx];
+
+ // Look up feed in the feedslist
+ final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter,
+ newFeed);
+ if (savedFeed == null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Found no existing Feed with title "
+ + newFeed.getTitle() + ". Adding as new one."
+ );
+ // Add a new Feed
+ newFeedsList.add(newFeed);
+ resultFeeds[feedIdx] = newFeed;
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Feed with title " + newFeed.getTitle()
+ + " already exists. Syncing new with existing one.");
+
+ Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
+ if (savedFeed.compareWithOther(newFeed)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Feed has updated attribute values. Updating old feed's attributes");
+ savedFeed.updateFromOther(newFeed);
+ }
+ if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences");
+ savedFeed.getPreferences().updateFromOther(newFeed.getPreferences());
+ }
+ // Look for new or updated Items
+ for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
+ final FeedItem item = newFeed.getItems().get(idx);
+ FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed,
+ item.getIdentifyingValue());
+ if (oldItem == null) {
+ // item is new
+ final int i = idx;
+ item.setFeed(savedFeed);
+ savedFeed.getItems().add(i, item);
+ item.setRead(false);
+ } else {
+ oldItem.updateFromOther(item);
+ }
+ }
+ // update attributes
+ savedFeed.setLastUpdate(newFeed.getLastUpdate());
+ savedFeed.setType(newFeed.getType());
+
+ updatedFeedsList.add(savedFeed);
+ resultFeeds[feedIdx] = savedFeed;
+ }
+ }
+
+ adapter.close();
+
+ try {
+ DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get();
+ DBWriter.setCompleteFeed(context, updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
+
+ return resultFeeds;
+ }
+
+ /**
+ * Searches the titles of FeedItems of a specific Feed for a given
+ * string.
+ *
+ * @param context Used for accessing the DB.
+ * @param feedID The id of the feed whose items should be searched.
+ * @param query The search string.
+ * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
+ */
+ public static FutureTask<List<FeedItem>> searchFeedItemTitle(final Context context,
+ final long feedID, final String query) {
+ return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
+ @Override
+ public void execute(PodDBAdapter adapter) {
+ Cursor searchResult = adapter.searchItemTitles(feedID,
+ query);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
+ DBReader.loadFeedDataOfFeedItemlist(context, items);
+ setResult(items);
+ searchResult.close();
+ }
+ });
+ }
+
+ /**
+ * Searches the descriptions of FeedItems of a specific Feed for a given
+ * string.
+ *
+ * @param context Used for accessing the DB.
+ * @param feedID The id of the feed whose items should be searched.
+ * @param query The search string
+ * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
+ */
+ public static FutureTask<List<FeedItem>> searchFeedItemDescription(final Context context,
+ final long feedID, final String query) {
+ return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
+ @Override
+ public void execute(PodDBAdapter adapter) {
+ Cursor searchResult = adapter.searchItemDescriptions(feedID,
+ query);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
+ DBReader.loadFeedDataOfFeedItemlist(context, items);
+ setResult(items);
+ searchResult.close();
+ }
+ });
+ }
+
+ /**
+ * Searches the contentEncoded-value of FeedItems of a specific Feed for a given
+ * string.
+ *
+ * @param context Used for accessing the DB.
+ * @param feedID The id of the feed whose items should be searched.
+ * @param query The search string
+ * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
+ */
+ public static FutureTask<List<FeedItem>> searchFeedItemContentEncoded(final Context context,
+ final long feedID, final String query) {
+ return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
+ @Override
+ public void execute(PodDBAdapter adapter) {
+ Cursor searchResult = adapter.searchItemContentEncoded(feedID,
+ query);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
+ DBReader.loadFeedDataOfFeedItemlist(context, items);
+ setResult(items);
+ searchResult.close();
+ }
+ });
+ }
+
+ /**
+ * Searches chapters of the FeedItems of a specific Feed for a given string.
+ *
+ * @param context Used for accessing the DB.
+ * @param feedID The id of the feed whose items should be searched.
+ * @param query The search string
+ * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
+ */
+ public static FutureTask<List<FeedItem>> searchFeedItemChapters(final Context context,
+ final long feedID, final String query) {
+ return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
+ @Override
+ public void execute(PodDBAdapter adapter) {
+ Cursor searchResult = adapter.searchItemChapters(feedID,
+ query);
+ List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
+ DBReader.loadFeedDataOfFeedItemlist(context, items);
+ setResult(items);
+ searchResult.close();
+ }
+ });
+ }
+
+ /**
+ * A runnable which should be used for database queries. The onCompletion
+ * method is executed on the database executor to handle Cursors correctly.
+ * This class automatically creates a PodDBAdapter object and closes it when
+ * it is no longer in use.
+ */
+ static abstract class QueryTask<T> implements Callable<T> {
+ private T result;
+ private Context context;
+
+ public QueryTask(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public T call() throws Exception {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ execute(adapter);
+ adapter.close();
+ return result;
+ }
+
+ public abstract void execute(PodDBAdapter adapter);
+
+ protected void setResult(T result) {
+ this.result = result;
+ }
+ }
+
+ /**
+ * Adds the given FeedItem to the flattr queue if the user is logged in. Otherwise, a dialog
+ * will be opened that lets the user go either to the login screen or the website of the flattr thing.
+ *
+ * @param context
+ * @param item
+ */
+ public static void flattrItemIfLoggedIn(Context context, FeedItem item) {
+ if (FlattrUtils.hasToken()) {
+ item.getFlattrStatus().setFlattrQueue();
+ DBWriter.setFlattredStatus(context, item, true);
+ } else {
+ FlattrUtils.showNoTokenDialogOrRedirect(context, item.getPaymentLink());
+ }
+ }
+
+ /**
+ * Adds the given Feed to the flattr queue if the user is logged in. Otherwise, a dialog
+ * will be opened that lets the user go either to the login screen or the website of the flattr thing.
+ *
+ * @param context
+ * @param feed
+ */
+ public static void flattrFeedIfLoggedIn(Context context, Feed feed) {
+ if (FlattrUtils.hasToken()) {
+ feed.getFlattrStatus().setFlattrQueue();
+ DBWriter.setFlattredStatus(context, feed, true);
+ } else {
+ FlattrUtils.showNoTokenDialogOrRedirect(context, feed.getPaymentLink());
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
new file mode 100644
index 000000000..d71d19433
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
@@ -0,0 +1,979 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.app.backup.BackupManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.asynctask.FlattrClickWorker;
+import de.danoeh.antennapod.core.feed.*;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+import de.danoeh.antennapod.core.util.flattr.FlattrThing;
+import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing;
+import org.shredzone.flattr4j.model.Flattr;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Provides methods for writing data to AntennaPod's database.
+ * In general, DBWriter-methods will be executed on an internal ExecutorService.
+ * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's
+ * will NOT contain any results.
+ * The caller can also use the {@link EventDistributor} in order to be notified about the method's completion asynchronously.
+ * This class will use the {@link EventDistributor} to notify listeners about changes in the database.
+ */
+public class DBWriter {
+ private static final String TAG = "DBWriter";
+
+ private static final ExecutorService dbExec;
+
+ static {
+ dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() {
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ });
+ }
+
+ private DBWriter() {
+ }
+
+ /**
+ * Deletes a downloaded FeedMedia file from the storage device.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param mediaId ID of the FeedMedia object whose downloaded file should be deleted.
+ */
+ public static Future<?> deleteFeedMediaOfItem(final Context context,
+ final long mediaId) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+
+ final FeedMedia media = DBReader.getFeedMedia(context, mediaId);
+ if (media != null) {
+ Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s",
+ media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded())));
+ boolean result = false;
+ if (media.isDownloaded()) {
+ // delete downloaded media file
+ File mediaFile = new File(media.getFile_url());
+ if (mediaFile.exists()) {
+ result = mediaFile.delete();
+ }
+ media.setDownloaded(false);
+ media.setFile_url(null);
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setMedia(media);
+ adapter.close();
+
+ // If media is currently being played, change playback
+ // type to 'stream' and shutdown playback service
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) {
+ if (media.getId() == PlaybackPreferences
+ .getCurrentlyPlayingFeedMediaId()) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
+ true);
+ editor.commit();
+ }
+ if (PlaybackPreferences
+ .getCurrentlyPlayingFeedMediaId() == media
+ .getId()) {
+ context.sendBroadcast(new Intent(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ }
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Deleting File. Result: " + result);
+ EventDistributor.getInstance().sendQueueUpdateBroadcast();
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ }
+ }
+ });
+ }
+
+ /**
+ * Deletes a Feed and all downloaded files of its components like images and downloaded episodes.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param feedId ID of the Feed that should be deleted.
+ */
+ public static Future<?> deleteFeed(final Context context, final long feedId) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ DownloadRequester requester = DownloadRequester.getInstance();
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(context
+ .getApplicationContext());
+ final Feed feed = DBReader.getFeed(context, feedId);
+ if (feed != null) {
+ if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA
+ && PlaybackPreferences.getLastPlayedFeedId() == feed
+ .getId()) {
+ context.sendBroadcast(new Intent(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ -1);
+ editor.commit();
+ }
+
+ // delete image file
+ if (feed.getImage() != null) {
+ if (feed.getImage().isDownloaded()
+ && feed.getImage().getFile_url() != null) {
+ File imageFile = new File(feed.getImage()
+ .getFile_url());
+ imageFile.delete();
+ } else if (requester.isDownloadingFile(feed.getImage())) {
+ requester.cancelDownload(context, feed.getImage());
+ }
+ }
+ // delete stored media files and mark them as read
+ List<FeedItem> queue = DBReader.getQueue(context);
+ boolean queueWasModified = false;
+ if (feed.getItems() == null) {
+ DBReader.getFeedItemList(context, feed);
+ }
+
+ for (FeedItem item : feed.getItems()) {
+ queueWasModified |= queue.remove(item);
+ if (item.getMedia() != null
+ && item.getMedia().isDownloaded()) {
+ File mediaFile = new File(item.getMedia()
+ .getFile_url());
+ mediaFile.delete();
+ } else if (item.getMedia() != null
+ && requester.isDownloadingFile(item.getMedia())) {
+ requester.cancelDownload(context, item.getMedia());
+ }
+
+ if (item.hasItemImage()) {
+ FeedImage image = item.getImage();
+ if (image.isDownloaded() && image.getFile_url() != null) {
+ File imgFile = new File(image.getFile_url());
+ imgFile.delete();
+ } else if (requester.isDownloadingFile(image)) {
+ requester.cancelDownload(context, item.getImage());
+ }
+ }
+ }
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ if (queueWasModified) {
+ adapter.setQueue(queue);
+ }
+ adapter.removeFeed(feed);
+ adapter.close();
+
+ if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
+ GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
+ }
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
+
+ BackupManager backupManager = new BackupManager(context);
+ backupManager.dataChanged();
+ }
+ }
+ });
+ }
+
+ /**
+ * Deletes the entire playback history.
+ *
+ * @param context A context that is used for opening a database connection.
+ */
+ public static Future<?> clearPlaybackHistory(final Context context) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.clearPlaybackHistory();
+ adapter.close();
+ EventDistributor.getInstance()
+ .sendPlaybackHistoryUpdateBroadcast();
+ }
+ });
+ }
+
+ /**
+ * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if
+ * its playback completion date is set to a non-null value. This method will set the playback completion date to the
+ * current date regardless of the current value.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param media FeedMedia that should be added to the playback history.
+ */
+ public static Future<?> addItemToPlaybackHistory(final Context context,
+ final FeedMedia media) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Adding new item to playback history");
+ media.setPlaybackCompletionDate(new Date());
+ // reset played_duration to 0 so that it behaves correctly when the episode is played again
+ media.setPlayedDuration(0);
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedMediaPlaybackCompletionDate(media);
+ adapter.close();
+ EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast();
+
+ }
+ });
+ }
+
+ private static void cleanupDownloadLog(final PodDBAdapter adapter) {
+ final long logSize = adapter.getDownloadLogSize();
+ if (logSize > DBReader.DOWNLOAD_LOG_SIZE) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Cleaning up download log");
+ adapter.removeDownloadLogItems(logSize - DBReader.DOWNLOAD_LOG_SIZE);
+ }
+ }
+
+ /**
+ * Adds a Download status object to the download log.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param status The DownloadStatus object.
+ */
+ public static Future<?> addDownloadStatus(final Context context,
+ final DownloadStatus status) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setDownloadStatus(status);
+ adapter.close();
+ EventDistributor.getInstance().sendDownloadLogUpdateBroadcast();
+ }
+ });
+
+ }
+
+ /**
+ * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to
+ * true. If the FeedItem is already in the queue, the queue will not be modified.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param itemId ID of the FeedItem that should be added to the queue.
+ * @param index Destination index. Must be in range 0..queue.size()
+ * @param performAutoDownload True if an auto-download process should be started after the operation
+ * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size()
+ */
+ public static Future<?> addQueueItemAt(final Context context, final long itemId,
+ final int index, final boolean performAutoDownload) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ final List<FeedItem> queue = DBReader
+ .getQueue(context, adapter);
+ FeedItem item = null;
+
+ if (queue != null) {
+ boolean queueModified = false;
+ boolean unreadItemsModified = false;
+
+ if (!itemListContains(queue, itemId)) {
+ item = DBReader.getFeedItem(context, itemId);
+ if (item != null) {
+ queue.add(index, item);
+ queueModified = true;
+ if (!item.isRead()) {
+ item.setRead(true);
+ unreadItemsModified = true;
+ }
+ }
+ }
+ if (queueModified) {
+ adapter.setQueue(queue);
+ EventDistributor.getInstance()
+ .sendQueueUpdateBroadcast();
+ }
+ if (unreadItemsModified && item != null) {
+ adapter.setSingleFeedItem(item);
+ EventDistributor.getInstance()
+ .sendUnreadItemsUpdateBroadcast();
+ }
+ }
+ adapter.close();
+ if (performAutoDownload) {
+ DBTasks.autodownloadUndownloadedItems(context);
+ }
+
+ }
+ });
+
+ }
+
+ /**
+ * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true.
+ * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param itemIds IDs of the FeedItem objects that should be added to the queue.
+ */
+ public static Future<?> addQueueItem(final Context context,
+ final long... itemIds) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ if (itemIds.length > 0) {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ final List<FeedItem> queue = DBReader.getQueue(context,
+ adapter);
+
+ if (queue != null) {
+ boolean queueModified = false;
+ boolean unreadItemsModified = false;
+ List<FeedItem> itemsToSave = new LinkedList<FeedItem>();
+ for (int i = 0; i < itemIds.length; i++) {
+ if (!itemListContains(queue, itemIds[i])) {
+ final FeedItem item = DBReader.getFeedItem(
+ context, itemIds[i]);
+
+ if (item != null) {
+ queue.add(item);
+ queueModified = true;
+ if (!item.isRead()) {
+ item.setRead(true);
+ itemsToSave.add(item);
+ unreadItemsModified = true;
+ }
+ }
+ }
+ }
+ if (queueModified) {
+ adapter.setQueue(queue);
+ EventDistributor.getInstance()
+ .sendQueueUpdateBroadcast();
+ }
+ if (unreadItemsModified) {
+ adapter.setFeedItemlist(itemsToSave);
+ EventDistributor.getInstance()
+ .sendUnreadItemsUpdateBroadcast();
+ }
+ }
+ adapter.close();
+ DBTasks.autodownloadUndownloadedItems(context);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * Removes all FeedItem objects from the queue.
+ *
+ * @param context A context that is used for opening a database connection.
+ */
+ public static Future<?> clearQueue(final Context context) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.clearQueue();
+ adapter.close();
+
+ EventDistributor.getInstance().sendQueueUpdateBroadcast();
+ }
+ });
+ }
+
+ /**
+ * Removes a FeedItem object from the queue.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param itemId ID of the FeedItem that should be removed.
+ * @param performAutoDownload true if an auto-download process should be started after the operation.
+ */
+ public static Future<?> removeQueueItem(final Context context,
+ final long itemId, final boolean performAutoDownload) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ final List<FeedItem> queue = DBReader
+ .getQueue(context, adapter);
+ FeedItem item = null;
+
+ if (queue != null) {
+ boolean queueModified = false;
+ QueueAccess queueAccess = QueueAccess.ItemListAccess(queue);
+ if (queueAccess.contains(itemId)) {
+ item = DBReader.getFeedItem(context, itemId);
+ if (item != null) {
+ queueModified = queueAccess.remove(itemId);
+ }
+ }
+ if (queueModified) {
+ adapter.setQueue(queue);
+ EventDistributor.getInstance()
+ .sendQueueUpdateBroadcast();
+ } else {
+ Log.w(TAG, "Queue was not modified by call to removeQueueItem");
+ }
+ } else {
+ Log.e(TAG, "removeQueueItem: Could not load queue");
+ }
+ adapter.close();
+ if (performAutoDownload) {
+ DBTasks.autodownloadUndownloadedItems(context);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * Moves the specified item to the top of the queue.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param itemId The item to move to the top of the queue
+ * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
+ * false if the caller wants to avoid unexpected updates of the GUI.
+ */
+ public static Future<?> moveQueueItemToTop(final Context context, final long itemId, final boolean broadcastUpdate) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ List<Long> queueIdList = DBReader.getQueueIDList(context);
+ int currentLocation = 0;
+ for (long id : queueIdList) {
+ if (id == itemId) {
+ moveQueueItemHelper(context, currentLocation, 0, broadcastUpdate);
+ return;
+ }
+ currentLocation++;
+ }
+ Log.e(TAG, "moveQueueItemToTop: item not found");
+ }
+ });
+ }
+
+ /**
+ * Moves the specified item to the bottom of the queue.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param itemId The item to move to the bottom of the queue
+ * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
+ * false if the caller wants to avoid unexpected updates of the GUI.
+ */
+ public static Future<?> moveQueueItemToBottom(final Context context, final long itemId,
+ final boolean broadcastUpdate) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ List<Long> queueIdList = DBReader.getQueueIDList(context);
+ int currentLocation = 0;
+ for (long id : queueIdList) {
+ if (id == itemId) {
+ moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1,
+ broadcastUpdate);
+ return;
+ }
+ currentLocation++;
+ }
+ Log.e(TAG, "moveQueueItemToBottom: item not found");
+ }
+ });
+ }
+
+ /**
+ * Changes the position of a FeedItem in the queue.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param from Source index. Must be in range 0..queue.size()-1.
+ * @param to Destination index. Must be in range 0..queue.size()-1.
+ * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
+ * false if the caller wants to avoid unexpected updates of the GUI.
+ * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size())
+ */
+ public static Future<?> moveQueueItem(final Context context, final int from,
+ final int to, final boolean broadcastUpdate) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ moveQueueItemHelper(context, from, to, broadcastUpdate);
+ }
+ });
+ }
+
+ /**
+ * Changes the position of a FeedItem in the queue.
+ * <p/>
+ * This function must be run using the ExecutorService (dbExec).
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param from Source index. Must be in range 0..queue.size()-1.
+ * @param to Destination index. Must be in range 0..queue.size()-1.
+ * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
+ * false if the caller wants to avoid unexpected updates of the GUI.
+ * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size())
+ */
+ private static void moveQueueItemHelper(final Context context, final int from,
+ final int to, final boolean broadcastUpdate) {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ final List<FeedItem> queue = DBReader
+ .getQueue(context, adapter);
+
+ if (queue != null) {
+ if (from >= 0 && from < queue.size() && to >= 0
+ && to < queue.size()) {
+
+ final FeedItem item = queue.remove(from);
+ queue.add(to, item);
+
+ adapter.setQueue(queue);
+ if (broadcastUpdate) {
+ EventDistributor.getInstance()
+ .sendQueueUpdateBroadcast();
+ }
+
+ }
+ } else {
+ Log.e(TAG, "moveQueueItemHelper: Could not load queue");
+ }
+ adapter.close();
+ }
+
+ /**
+ * Sets the 'read'-attribute of a FeedItem to the specified value.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param item The FeedItem object
+ * @param read New value of the 'read'-attribute
+ * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object.
+ * If the FeedItem has no FeedMedia object, this parameter will be ignored.
+ */
+ public static Future<?> markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) {
+ long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0;
+ return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition);
+ }
+
+ /**
+ * Sets the 'read'-attribute of a FeedItem to the specified value.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param itemId ID of the FeedItem
+ * @param read New value of the 'read'-attribute
+ */
+ public static Future<?> markItemRead(final Context context, final long itemId,
+ final boolean read) {
+ return markItemRead(context, itemId, read, 0, false);
+ }
+
+ private static Future<?> markItemRead(final Context context, final long itemId,
+ final boolean read, final long mediaId,
+ final boolean resetMediaPosition) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedItemRead(read, itemId, mediaId,
+ resetMediaPosition);
+ adapter.close();
+
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ }
+ });
+ }
+
+ /**
+ * Sets the 'read'-attribute of all FeedItems of a specific Feed to true.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param feedId ID of the Feed.
+ */
+ public static Future<?> markFeedRead(final Context context, final long feedId) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId);
+ long[] itemIds = new long[itemCursor.getCount()];
+ itemCursor.moveToFirst();
+ for (int i = 0; i < itemIds.length; i++) {
+ itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
+ itemCursor.moveToNext();
+ }
+ itemCursor.close();
+ adapter.setFeedItemRead(true, itemIds);
+ adapter.close();
+
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ }
+ });
+
+ }
+
+ /**
+ * Sets the 'read'-attribute of all FeedItems to true.
+ *
+ * @param context A context that is used for opening a database connection.
+ */
+ public static Future<?> markAllItemsRead(final Context context) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ Cursor itemCursor = adapter.getUnreadItemsCursor();
+ long[] itemIds = new long[itemCursor.getCount()];
+ itemCursor.moveToFirst();
+ for (int i = 0; i < itemIds.length; i++) {
+ itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
+ itemCursor.moveToNext();
+ }
+ itemCursor.close();
+ adapter.setFeedItemRead(true, itemIds);
+ adapter.close();
+
+ EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
+ }
+ });
+
+ }
+
+ static Future<?> addNewFeed(final Context context, final Feed... feeds) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ final PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feeds);
+ adapter.close();
+
+ if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
+ for (Feed feed : feeds) {
+ GpodnetPreferences.addAddedFeed(feed.getDownload_url());
+ }
+ }
+
+ BackupManager backupManager = new BackupManager(context);
+ backupManager.dataChanged();
+ }
+ });
+ }
+
+ static Future<?> setCompleteFeed(final Context context, final Feed... feeds) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feeds);
+ adapter.close();
+
+ }
+ });
+
+ }
+
+ /**
+ * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The
+ * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param media The FeedMedia object.
+ */
+ public static Future<?> setFeedMedia(final Context context,
+ final FeedMedia media) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setMedia(media);
+ adapter.close();
+ }
+ });
+ }
+
+ /**
+ * Saves the 'position' and 'duration' attributes of a FeedMedia object
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param media The FeedMedia object.
+ */
+ public static Future<?> setFeedMediaPlaybackInformation(final Context context, final FeedMedia media) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedMediaPlaybackInformation(media);
+ adapter.close();
+ }
+ });
+ }
+
+ /**
+ * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including
+ * the content of FeedComponent-attributes.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param item The FeedItem object.
+ */
+ public static Future<?> setFeedItem(final Context context,
+ final FeedItem item) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setSingleFeedItem(item);
+ adapter.close();
+ }
+ });
+ }
+
+ /**
+ * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The
+ * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved.
+ *
+ * @param context A context that is used for opening a database connection.
+ * @param image The FeedImage object.
+ */
+ public static Future<?> setFeedImage(final Context context,
+ final FeedImage image) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setImage(image);
+ adapter.close();
+ }
+ });
+ }
+
+ /**
+ * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed
+ * and the value is the updated URL
+ */
+ public static Future<?> updateFeedDownloadURLs(final Context context, final Map<String, String> urls) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ for (String key : urls.keySet()) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key));
+
+ adapter.setFeedDownloadUrl(key, urls.get(key));
+ }
+ adapter.close();
+ }
+ });
+ }
+
+ /**
+ * Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0.
+ *
+ * @param context Used for opening a database connection.
+ * @param preferences The FeedPreferences object.
+ */
+ public static Future<?> setFeedPreferences(final Context context, final FeedPreferences preferences) {
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedPreferences(preferences);
+ adapter.close();
+ EventDistributor.getInstance().sendFeedUpdateBroadcast();
+ }
+ });
+ }
+
+ private static boolean itemListContains(List<FeedItem> items, long itemId) {
+ for (FeedItem item : items) {
+ if (item.getId() == itemId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Saves the FlattrStatus of a FeedItem object in the database.
+ *
+ * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
+ */
+ public static Future<?> setFeedItemFlattrStatus(final Context context,
+ final FeedItem item,
+ final boolean startFlattrClickWorker) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedItemFlattrStatus(item);
+ adapter.close();
+ if (startFlattrClickWorker) {
+ new FlattrClickWorker(context).executeAsync();
+ }
+ }
+ });
+ }
+
+ /**
+ * Saves the FlattrStatus of a Feed object in the database.
+ *
+ * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
+ */
+ private static Future<?> setFeedFlattrStatus(final Context context,
+ final Feed feed,
+ final boolean startFlattrClickWorker) {
+ return dbExec.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setFeedFlattrStatus(feed);
+ adapter.close();
+ if (startFlattrClickWorker) {
+ new FlattrClickWorker(context).executeAsync();
+ }
+ }
+ });
+ }
+
+ /**
+ * format an url for querying the database
+ * (postfix a / and apply percent-encoding)
+ */
+ private static String formatURIForQuery(String uri) {
+ try {
+ return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, e.getMessage());
+ return "";
+ }
+ }
+
+
+ /**
+ * Set flattr status of the passed thing (either a FeedItem or a Feed)
+ *
+ * @param context
+ * @param thing
+ * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
+ * @return
+ */
+ public static Future<?> setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) {
+ // must propagate this to back db
+ if (thing instanceof FeedItem)
+ return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker);
+ else if (thing instanceof Feed)
+ return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker);
+ else if (thing instanceof SimpleFlattrThing) {
+ } // SimpleFlattrThings are generated on the fly and do not have DB backing
+ else
+ Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing");
+
+ return null;
+ }
+
+ /**
+ * Reset flattr status to unflattrd for all items
+ */
+ public static Future<?> clearAllFlattrStatus(final Context context) {
+ Log.d(TAG, "clearAllFlattrStatus()");
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.clearAllFlattrStatus();
+ adapter.close();
+ }
+ });
+ }
+
+ /**
+ * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp,
+ * where the information has been retrieved from the flattr API
+ */
+ public static Future<?> setFlattredStatus(final Context context, final List<Flattr> flattrList) {
+ Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items");
+ // clear flattr status in db
+ clearAllFlattrStatus(context);
+
+ // submit list with flattred things having normalized URLs to db
+ return dbExec.submit(new Runnable() {
+ @Override
+ public void run() {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ for (Flattr flattr : flattrList) {
+ adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime()));
+ }
+ adapter.close();
+ }
+ });
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java
new file mode 100644
index 000000000..c85559e20
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.core.storage;
+
+/**
+ * Thrown by the DownloadRequester if a download request contains invalid data
+ * or something went wrong while processing the request.
+ */
+public class DownloadRequestException extends Exception {
+
+ public DownloadRequestException() {
+ super();
+ }
+
+ public DownloadRequestException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public DownloadRequestException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public DownloadRequestException(Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
new file mode 100644
index 000000000..f0331e997
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
@@ -0,0 +1,366 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.webkit.URLUtil;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.*;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.core.util.FileNameGenerator;
+import de.danoeh.antennapod.core.util.URLChecker;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
+import java.io.File;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+
+/**
+ * Sends download requests to the DownloadService. This class should always be used for starting downloads,
+ * otherwise they won't work correctly.
+ */
+public class DownloadRequester {
+ private static final String TAG = "DownloadRequester";
+
+ public static final String IMAGE_DOWNLOADPATH = "images/";
+ public static final String FEED_DOWNLOADPATH = "cache/";
+ public static final String MEDIA_DOWNLOADPATH = "media/";
+
+ private static DownloadRequester downloader;
+
+ private Map<String, DownloadRequest> downloads;
+
+ private DownloadRequester() {
+ downloads = new ConcurrentHashMap<String, DownloadRequest>();
+ }
+
+ public static synchronized DownloadRequester getInstance() {
+ if (downloader == null) {
+ downloader = new DownloadRequester();
+ }
+ return downloader;
+ }
+
+ /**
+ * Starts a new download with the given DownloadRequest. This method should only
+ * be used from outside classes if the DownloadRequest was created by the DownloadService to
+ * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead.
+ *
+ * @param context Context object for starting the DownloadService
+ * @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method
+ * call will return false.
+ * @return True if the download request was accepted, false otherwise.
+ */
+ public synchronized boolean download(Context context, DownloadRequest request) {
+ Validate.notNull(context);
+ Validate.notNull(request);
+
+ if (downloads.containsKey(request.getSource())) {
+ if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored.");
+ return false;
+ }
+ downloads.put(request.getSource(), request);
+
+ Intent launchIntent = new Intent(context, DownloadService.class);
+ launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
+ context.startService(launchIntent);
+ EventDistributor.getInstance().sendDownloadQueuedBroadcast();
+ return true;
+ }
+
+ private void download(Context context, FeedFile item, File dest,
+ boolean overwriteIfExists, String username, String password, boolean deleteOnFailure) {
+ if (!isDownloadingFile(item)) {
+ if (!isFilenameAvailable(dest.toString()) || (deleteOnFailure && dest.exists())) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Filename already used.");
+ if (isFilenameAvailable(dest.toString()) && overwriteIfExists) {
+ boolean result = dest.delete();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Deleting file. Result: " + result);
+ } else {
+ // find different name
+ File newDest = null;
+ for (int i = 1; i < Integer.MAX_VALUE; i++) {
+ String newName = FilenameUtils.getBaseName(dest
+ .getName())
+ + "-"
+ + i
+ + FilenameUtils.EXTENSION_SEPARATOR
+ + FilenameUtils.getExtension(dest.getName());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Testing filename " + newName);
+ newDest = new File(dest.getParent(), newName);
+ if (!newDest.exists()
+ && isFilenameAvailable(newDest.toString())) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "File doesn't exist yet. Using "
+ + newName);
+ break;
+ }
+ }
+ if (newDest != null) {
+ dest = newDest;
+ }
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Requesting download of url " + item.getDownload_url());
+ item.setDownload_url(URLChecker.prepareURL(item.getDownload_url()));
+
+ DownloadRequest request = new DownloadRequest(dest.toString(),
+ URLChecker.prepareURL(item.getDownload_url()), item.getHumanReadableIdentifier(),
+ item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure);
+
+ download(context, request);
+ } else {
+ Log.e(TAG, "URL " + item.getDownload_url()
+ + " is already being downloaded");
+ }
+ }
+
+ /**
+ * Returns true if a filename is available and false if it has already been
+ * taken by another requested download.
+ */
+ private boolean isFilenameAvailable(String path) {
+ for (String key : downloads.keySet()) {
+ DownloadRequest r = downloads.get(key);
+ if (StringUtils.equals(r.getDestination(), path)) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, path
+ + " is already used by another requested download");
+ return false;
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, path + " is available as a download destination");
+ return true;
+ }
+
+ public synchronized void downloadFeed(Context context, Feed feed)
+ throws DownloadRequestException {
+ if (feedFileValid(feed)) {
+ String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null;
+ String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null;
+
+ download(context, feed, new File(getFeedfilePath(context),
+ getFeedfileName(feed)), true, username, password, true);
+ }
+ }
+
+ public synchronized void downloadImage(Context context, FeedImage image)
+ throws DownloadRequestException {
+ if (feedFileValid(image)) {
+ download(context, image, new File(getImagefilePath(context),
+ getImagefileName(image)), false, null, null, false);
+ }
+ }
+
+ public synchronized void downloadMedia(Context context, FeedMedia feedmedia)
+ throws DownloadRequestException {
+ if (feedFileValid(feedmedia)) {
+ Feed feed = feedmedia.getItem().getFeed();
+ String username;
+ String password;
+ if (feed != null && feed.getPreferences() != null) {
+ username = feed.getPreferences().getUsername();
+ password = feed.getPreferences().getPassword();
+ } else {
+ username = null;
+ password = null;
+ }
+
+ File dest;
+ if (feedmedia.getFile_url() != null) {
+ dest = new File(feedmedia.getFile_url());
+ } else {
+ dest = new File(getMediafilePath(context, feedmedia),
+ getMediafilename(feedmedia));
+ }
+ download(context, feedmedia,
+ dest, false, username, password, false
+ );
+ }
+ }
+
+ /**
+ * Throws a DownloadRequestException if the feedfile or the download url of
+ * the feedfile is null.
+ *
+ * @throws DownloadRequestException
+ */
+ private boolean feedFileValid(FeedFile f) throws DownloadRequestException {
+ if (f == null) {
+ throw new DownloadRequestException("Feedfile was null");
+ } else if (f.getDownload_url() == null) {
+ throw new DownloadRequestException("File has no download URL");
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Cancels a running download.
+ */
+ public synchronized void cancelDownload(final Context context, final FeedFile f) {
+ cancelDownload(context, f.getDownload_url());
+ }
+
+ /**
+ * Cancels a running download.
+ */
+ public synchronized void cancelDownload(final Context context, final String downloadUrl) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Cancelling download with url " + downloadUrl);
+ Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD);
+ cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl);
+ context.sendBroadcast(cancelIntent);
+ }
+
+ /**
+ * Cancels all running downloads
+ */
+ public synchronized void cancelAllDownloads(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Cancelling all running downloads");
+ context.sendBroadcast(new Intent(
+ DownloadService.ACTION_CANCEL_ALL_DOWNLOADS));
+ }
+
+ /**
+ * Returns true if there is at least one Feed in the downloads queue.
+ */
+ public synchronized boolean isDownloadingFeeds() {
+ for (DownloadRequest r : downloads.values()) {
+ if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if feedfile is in the downloads list
+ */
+ public synchronized boolean isDownloadingFile(FeedFile item) {
+ if (item.getDownload_url() != null) {
+ return downloads.containsKey(item.getDownload_url());
+ }
+ return false;
+ }
+
+ public synchronized DownloadRequest getDownload(String downloadUrl) {
+ return downloads.get(downloadUrl);
+ }
+
+ /**
+ * Checks if feedfile with the given download url is in the downloads list
+ */
+ public synchronized boolean isDownloadingFile(String downloadUrl) {
+ return downloads.get(downloadUrl) != null;
+ }
+
+ public synchronized boolean hasNoDownloads() {
+ return downloads.isEmpty();
+ }
+
+ /**
+ * Remove an object from the downloads-list of the requester.
+ */
+ public synchronized void removeDownload(DownloadRequest r) {
+ if (downloads.remove(r.getSource()) == null) {
+ Log.e(TAG,
+ "Could not remove object with url " + r.getSource());
+ }
+ }
+
+ /**
+ * Get the number of uncompleted Downloads
+ */
+ public synchronized int getNumberOfDownloads() {
+ return downloads.size();
+ }
+
+ public synchronized String getFeedfilePath(Context context)
+ throws DownloadRequestException {
+ return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH)
+ .toString() + "/";
+ }
+
+ public synchronized String getFeedfileName(Feed feed) {
+ String filename = feed.getDownload_url();
+ if (feed.getTitle() != null && !feed.getTitle().isEmpty()) {
+ filename = feed.getTitle();
+ }
+ return "feed-" + FileNameGenerator.generateFileName(filename);
+ }
+
+ public synchronized String getImagefilePath(Context context)
+ throws DownloadRequestException {
+ return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH)
+ .toString() + "/";
+ }
+
+ public synchronized String getImagefileName(FeedImage image) {
+ String filename = image.getDownload_url();
+ if (image.getOwner() != null && image.getOwner().getHumanReadableIdentifier() != null) {
+ filename = image.getOwner().getHumanReadableIdentifier();
+ }
+ return "image-" + FileNameGenerator.generateFileName(filename);
+ }
+
+ public synchronized String getMediafilePath(Context context, FeedMedia media)
+ throws DownloadRequestException {
+ File externalStorage = getExternalFilesDirOrThrowException(
+ context,
+ MEDIA_DOWNLOADPATH
+ + FileNameGenerator.generateFileName(media.getItem()
+ .getFeed().getTitle()) + "/"
+ );
+ return externalStorage.toString();
+ }
+
+ private File getExternalFilesDirOrThrowException(Context context,
+ String type) throws DownloadRequestException {
+ File result = UserPreferences.getDataFolder(context, type);
+ if (result == null) {
+ throw new DownloadRequestException(
+ "Failed to access external storage");
+ }
+ return result;
+ }
+
+ private String getMediafilename(FeedMedia media) {
+ String filename;
+ String titleBaseFilename = "";
+
+ // Try to generate the filename by the item title
+ if (media.getItem() != null && media.getItem().getTitle() != null) {
+ String title = media.getItem().getTitle();
+ // Delete reserved characters
+ titleBaseFilename = title.replaceAll("[\\\\/%\\?\\*:|<>\"\\p{Cntrl}]", "");
+ titleBaseFilename = titleBaseFilename.trim();
+ }
+
+ String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(),
+ null, media.getMime_type());
+ ;
+
+ if (titleBaseFilename != "") {
+ // Append extension
+ filename = titleBaseFilename + FilenameUtils.EXTENSION_SEPARATOR +
+ FilenameUtils.getExtension(URLBaseFilename);
+ } else {
+ // Fall back on URL file name
+ filename = URLBaseFilename;
+ }
+ return filename;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java
new file mode 100644
index 000000000..f6a59836b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java
@@ -0,0 +1,70 @@
+package de.danoeh.antennapod.core.storage;
+
+import java.util.Date;
+
+/**
+ * Contains information about a feed's items.
+ */
+public class FeedItemStatistics {
+ private long feedID;
+ private int numberOfItems;
+ private int numberOfNewItems;
+ private int numberOfInProgressItems;
+ private Date lastUpdate;
+ private static final Date UNKNOWN_DATE = new Date(0);
+
+
+ /**
+ * Creates new FeedItemStatistics object.
+ *
+ * @param feedID ID of the feed.
+ * @param numberOfItems Number of items that this feed has.
+ * @param numberOfNewItems Number of unread items this feed has.
+ * @param numberOfInProgressItems Number of items that the user has started listening to.
+ * @param lastUpdate pubDate of the latest episode. A lastUpdate value of 0 will be interpreted as DATE_UNKOWN if
+ * numberOfItems is 0.
+ */
+ public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) {
+ this.feedID = feedID;
+ this.numberOfItems = numberOfItems;
+ this.numberOfNewItems = numberOfNewItems;
+ this.numberOfInProgressItems = numberOfInProgressItems;
+ if (numberOfItems > 0) {
+ this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
+ } else {
+ this.lastUpdate = UNKNOWN_DATE;
+ }
+ }
+
+ public long getFeedID() {
+ return feedID;
+ }
+
+ public int getNumberOfItems() {
+ return numberOfItems;
+ }
+
+ public int getNumberOfNewItems() {
+ return numberOfNewItems;
+ }
+
+ public int getNumberOfInProgressItems() {
+ return numberOfInProgressItems;
+ }
+
+ /**
+ * Returns the pubDate of the latest item in the feed. Users of this method
+ * should check if this value is unkown or not by calling lastUpdateKnown() first.
+ */
+ public Date getLastUpdate() {
+ return (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
+ }
+
+ /**
+ * Returns true if the lastUpdate value is known. The lastUpdate value is unkown if the
+ * feed has no items.
+ */
+ public boolean lastUpdateKnown() {
+ return lastUpdate != UNKNOWN_DATE;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
new file mode 100644
index 000000000..3a63685ba
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
@@ -0,0 +1,57 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.SearchResult;
+import de.danoeh.antennapod.core.util.comparator.SearchResultValueComparator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Performs search on Feeds and FeedItems
+ */
+public class FeedSearcher {
+ private static final String TAG = "FeedSearcher";
+
+
+ /**
+ * Performs a search in all feeds or one specific feed.
+ */
+ public static List<SearchResult> performSearch(final Context context,
+ final String query, final long selectedFeed) {
+ final int values[] = {0, 0, 1, 2};
+ final String[] subtitles = {context.getString(R.string.found_in_shownotes_label),
+ context.getString(R.string.found_in_shownotes_label),
+ context.getString(R.string.found_in_chapters_label),
+ context.getString(R.string.found_in_title_label)};
+
+ List<SearchResult> result = new ArrayList<SearchResult>();
+
+ FutureTask<List<FeedItem>>[] tasks = new FutureTask[4];
+ (tasks[0] = DBTasks.searchFeedItemContentEncoded(context, selectedFeed, query)).run();
+ (tasks[1] = DBTasks.searchFeedItemDescription(context, selectedFeed, query)).run();
+ (tasks[2] = DBTasks.searchFeedItemChapters(context, selectedFeed, query)).run();
+ (tasks[3] = DBTasks.searchFeedItemTitle(context, selectedFeed, query)).run();
+ try {
+ for (int i = 0; i < tasks.length; i++) {
+ FutureTask task = tasks[i];
+ List<FeedItem> items = (List<FeedItem>) task.get();
+ for (FeedItem item : items) {
+ result.add(new SearchResult(item, values[i], subtitles[i]));
+ }
+
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ Collections.sort(result, new SearchResultValueComparator());
+ return result;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
new file mode 100644
index 000000000..6b6e09369
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
@@ -0,0 +1,1310 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.MergeCursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import org.apache.commons.lang3.Validate;
+
+import java.util.Arrays;
+import java.util.List;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedComponent;
+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.feed.FeedPreferences;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
+
+// TODO Remove media column from feeditem table
+
+/**
+ * Implements methods for accessing the database
+ */
+public class PodDBAdapter {
+ private static final String TAG = "PodDBAdapter";
+ public static final String DATABASE_NAME = "Antennapod.db";
+
+ /**
+ * Maximum number of arguments for IN-operator.
+ */
+ public static final int IN_OPERATOR_MAXIMUM = 800;
+
+ /**
+ * Maximum number of entries per search request.
+ */
+ public static final int SEARCH_LIMIT = 30;
+
+ // ----------- Column indices
+ // ----------- General indices
+ public static final int KEY_ID_INDEX = 0;
+ public static final int KEY_TITLE_INDEX = 1;
+ public static final int KEY_FILE_URL_INDEX = 2;
+ public static final int KEY_DOWNLOAD_URL_INDEX = 3;
+ public static final int KEY_DOWNLOADED_INDEX = 4;
+ public static final int KEY_LINK_INDEX = 5;
+ public static final int KEY_DESCRIPTION_INDEX = 6;
+ public static final int KEY_PAYMENT_LINK_INDEX = 7;
+ // ----------- Feed indices
+ public static final int KEY_LAST_UPDATE_INDEX = 8;
+ public static final int KEY_LANGUAGE_INDEX = 9;
+ public static final int KEY_AUTHOR_INDEX = 10;
+ public static final int KEY_IMAGE_INDEX = 11;
+ public static final int KEY_TYPE_INDEX = 12;
+ public static final int KEY_FEED_IDENTIFIER_INDEX = 13;
+ public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14;
+ public static final int KEY_FEED_USERNAME_INDEX = 15;
+ public static final int KEY_FEED_PASSWORD_INDEX = 16;
+ // ----------- FeedItem indices
+ public static final int KEY_CONTENT_ENCODED_INDEX = 2;
+ public static final int KEY_PUBDATE_INDEX = 3;
+ public static final int KEY_READ_INDEX = 4;
+ public static final int KEY_MEDIA_INDEX = 8;
+ public static final int KEY_FEED_INDEX = 9;
+ public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10;
+ public static final int KEY_ITEM_IDENTIFIER_INDEX = 11;
+ public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12;
+ // ---------- FeedMedia indices
+ public static final int KEY_DURATION_INDEX = 1;
+ public static final int KEY_POSITION_INDEX = 5;
+ public static final int KEY_SIZE_INDEX = 6;
+ public static final int KEY_MIME_TYPE_INDEX = 7;
+ public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8;
+ public static final int KEY_MEDIA_FEEDITEM_INDEX = 9;
+ public static final int KEY_PLAYED_DURATION_INDEX = 10;
+ // --------- Download log indices
+ public static final int KEY_FEEDFILE_INDEX = 1;
+ public static final int KEY_FEEDFILETYPE_INDEX = 2;
+ public static final int KEY_REASON_INDEX = 3;
+ public static final int KEY_SUCCESSFUL_INDEX = 4;
+ public static final int KEY_COMPLETION_DATE_INDEX = 5;
+ public static final int KEY_REASON_DETAILED_INDEX = 6;
+ public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7;
+ // --------- Queue indices
+ public static final int KEY_FEEDITEM_INDEX = 1;
+ public static final int KEY_QUEUE_FEED_INDEX = 2;
+ // --------- Chapters indices
+ public static final int KEY_CHAPTER_START_INDEX = 2;
+ public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3;
+ public static final int KEY_CHAPTER_LINK_INDEX = 4;
+ public static final int KEY_CHAPTER_TYPE_INDEX = 5;
+
+ // Key-constants
+ public static final String KEY_ID = "id";
+ public static final String KEY_TITLE = "title";
+ public static final String KEY_NAME = "name";
+ public static final String KEY_LINK = "link";
+ public static final String KEY_DESCRIPTION = "description";
+ public static final String KEY_FILE_URL = "file_url";
+ public static final String KEY_DOWNLOAD_URL = "download_url";
+ public static final String KEY_PUBDATE = "pubDate";
+ public static final String KEY_READ = "read";
+ public static final String KEY_DURATION = "duration";
+ public static final String KEY_POSITION = "position";
+ public static final String KEY_SIZE = "filesize";
+ public static final String KEY_MIME_TYPE = "mime_type";
+ public static final String KEY_IMAGE = "image";
+ public static final String KEY_FEED = "feed";
+ public static final String KEY_MEDIA = "media";
+ public static final String KEY_DOWNLOADED = "downloaded";
+ public static final String KEY_LASTUPDATE = "last_update";
+ public static final String KEY_FEEDFILE = "feedfile";
+ public static final String KEY_REASON = "reason";
+ public static final String KEY_SUCCESSFUL = "successful";
+ public static final String KEY_FEEDFILETYPE = "feedfile_type";
+ public static final String KEY_COMPLETION_DATE = "completion_date";
+ public static final String KEY_FEEDITEM = "feeditem";
+ public static final String KEY_CONTENT_ENCODED = "content_encoded";
+ public static final String KEY_PAYMENT_LINK = "payment_link";
+ public static final String KEY_START = "start";
+ public static final String KEY_LANGUAGE = "language";
+ public static final String KEY_AUTHOR = "author";
+ public static final String KEY_HAS_CHAPTERS = "has_simple_chapters";
+ public static final String KEY_TYPE = "type";
+ public static final String KEY_ITEM_IDENTIFIER = "item_identifier";
+ public static final String KEY_FLATTR_STATUS = "flattr_status";
+ public static final String KEY_FEED_IDENTIFIER = "feed_identifier";
+ public static final String KEY_REASON_DETAILED = "reason_detailed";
+ public static final String KEY_DOWNLOADSTATUS_TITLE = "title";
+ public static final String KEY_CHAPTER_TYPE = "type";
+ public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date";
+ public static final String KEY_AUTO_DOWNLOAD = "auto_download";
+ public static final String KEY_PLAYED_DURATION = "played_duration";
+ public static final String KEY_USERNAME = "username";
+ public static final String KEY_PASSWORD = "password";
+
+ // Table names
+ public static final String TABLE_NAME_FEEDS = "Feeds";
+ public static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
+ public static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
+ public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
+ public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
+ public static final String TABLE_NAME_QUEUE = "Queue";
+ public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
+
+ // SQL Statements for creating new tables
+ private static final String TABLE_PRIMARY_KEY = KEY_ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT ,";
+
+ public static final String CREATE_TABLE_FEEDS = "CREATE TABLE "
+ + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT,"
+ + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR
+ + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT,"
+ + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1,"
+ + KEY_FLATTR_STATUS + " INTEGER,"
+ + KEY_USERNAME + " TEXT,"
+ + KEY_PASSWORD + " TEXT)";
+
+ public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE "
+ + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE
+ + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT,"
+ + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
+ + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
+ + KEY_FLATTR_STATUS + " INTEGER,"
+ + KEY_IMAGE + " INTEGER)";
+
+ public static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE "
+ + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ + KEY_DOWNLOADED + " INTEGER)";
+
+ public static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
+ + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
+ + " INTEGER," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL
+ + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION
+ + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT,"
+ + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER,"
+ + KEY_FEEDITEM + " INTEGER,"
+ + KEY_PLAYED_DURATION + " INTEGER)";
+
+ public static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE "
+ + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE
+ + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON
+ + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE
+ + " INTEGER," + KEY_REASON_DETAILED + " TEXT,"
+ + KEY_DOWNLOADSTATUS_TITLE + " TEXT)";
+
+ public static final String CREATE_TABLE_QUEUE = "CREATE TABLE "
+ + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY,"
+ + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)";
+
+ public static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE "
+ + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER,"
+ + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)";
+
+ private SQLiteDatabase db;
+ private final Context context;
+ private PodDBHelper helper;
+
+ /**
+ * Select all columns from the feed-table
+ */
+ private static final String[] FEED_SEL_STD = {
+ TABLE_NAME_FEEDS + "." + KEY_ID,
+ TABLE_NAME_FEEDS + "." + KEY_TITLE,
+ TABLE_NAME_FEEDS + "." + KEY_FILE_URL,
+ TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL,
+ TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED,
+ TABLE_NAME_FEEDS + "." + KEY_LINK,
+ TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION,
+ TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK,
+ TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE,
+ TABLE_NAME_FEEDS + "." + KEY_LANGUAGE,
+ TABLE_NAME_FEEDS + "." + KEY_AUTHOR,
+ TABLE_NAME_FEEDS + "." + KEY_IMAGE,
+ TABLE_NAME_FEEDS + "." + KEY_TYPE,
+ TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
+ TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD,
+ TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS,
+ TABLE_NAME_FEEDS + "." + KEY_USERNAME,
+ TABLE_NAME_FEEDS + "." + KEY_PASSWORD
+ };
+
+ // column indices for FEED_SEL_STD
+ public static final int IDX_FEED_SEL_STD_ID = 0;
+ public static final int IDX_FEED_SEL_STD_TITLE = 1;
+ public static final int IDX_FEED_SEL_STD_FILE_URL = 2;
+ public static final int IDX_FEED_SEL_STD_DOWNLOAD_URL = 3;
+ public static final int IDX_FEED_SEL_STD_DOWNLOADED = 4;
+ public static final int IDX_FEED_SEL_STD_LINK = 5;
+ public static final int IDX_FEED_SEL_STD_DESCRIPTION = 6;
+ public static final int IDX_FEED_SEL_STD_PAYMENT_LINK = 7;
+ public static final int IDX_FEED_SEL_STD_LASTUPDATE = 8;
+ public static final int IDX_FEED_SEL_STD_LANGUAGE = 9;
+ public static final int IDX_FEED_SEL_STD_AUTHOR = 10;
+ public static final int IDX_FEED_SEL_STD_IMAGE = 11;
+ public static final int IDX_FEED_SEL_STD_TYPE = 12;
+ public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13;
+ public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14;
+ public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15;
+ public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 16;
+ public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 17;
+
+
+ /**
+ * Select all columns from the feeditems-table except description and
+ * content-encoded.
+ */
+ private static final String[] FEEDITEM_SEL_FI_SMALL = {
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_READ,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_LINK,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_FEED,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE};
+
+ /**
+ * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries.
+ */
+ private static final String SEL_FI_SMALL_STR;
+
+ static {
+ String selFiSmall = Arrays.toString(FEEDITEM_SEL_FI_SMALL);
+ SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1);
+ }
+
+ // column indices for FEEDITEM_SEL_FI_SMALL
+
+ public static final int IDX_FI_SMALL_ID = 0;
+ public static final int IDX_FI_SMALL_TITLE = 1;
+ public static final int IDX_FI_SMALL_PUBDATE = 2;
+ public static final int IDX_FI_SMALL_READ = 3;
+ public static final int IDX_FI_SMALL_LINK = 4;
+ public static final int IDX_FI_SMALL_PAYMENT_LINK = 5;
+ public static final int IDX_FI_SMALL_MEDIA = 6;
+ public static final int IDX_FI_SMALL_FEED = 7;
+ public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8;
+ public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9;
+ public static final int IDX_FI_SMALL_FLATTR_STATUS = 10;
+ public static final int IDX_FI_SMALL_IMAGE = 11;
+
+ /**
+ * Select id, description and content-encoded column from feeditems.
+ */
+ private static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION,
+ KEY_CONTENT_ENCODED, KEY_FEED};
+
+ // column indices for SEL_FI_EXTRA
+
+ public static final int IDX_FI_EXTRA_ID = 0;
+ public static final int IDX_FI_EXTRA_DESCRIPTION = 1;
+ public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2;
+ public static final int IDX_FI_EXTRA_FEED = 3;
+
+ static PodDBHelper dbHelperSingleton;
+
+ private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) {
+ if (dbHelperSingleton == null) {
+ dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null,
+ ClientConfig.storageCallbacks.getDatabaseVersion());
+ }
+ return dbHelperSingleton;
+ }
+
+ public PodDBAdapter(Context c) {
+ this.context = c;
+ helper = getDbHelperSingleton(c.getApplicationContext());
+ }
+
+ public PodDBAdapter open() {
+ if (db == null || !db.isOpen() || db.isReadOnly()) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Opening DB");
+ try {
+ db = helper.getWritableDatabase();
+ } catch (SQLException ex) {
+ ex.printStackTrace();
+ db = helper.getReadableDatabase();
+ }
+ }
+ return this;
+ }
+
+ public void close() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Closing DB");
+ //db.close();
+ }
+
+ public static boolean deleteDatabase(Context context) {
+ Log.w(TAG, "Deleting database");
+ dbHelperSingleton.close();
+ dbHelperSingleton = null;
+ return context.deleteDatabase(DATABASE_NAME);
+ }
+
+ /**
+ * Inserts or updates a feed entry
+ *
+ * @return the id of the entry
+ */
+ public long setFeed(Feed feed) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, feed.getTitle());
+ values.put(KEY_LINK, feed.getLink());
+ values.put(KEY_DESCRIPTION, feed.getDescription());
+ values.put(KEY_PAYMENT_LINK, feed.getPaymentLink());
+ values.put(KEY_AUTHOR, feed.getAuthor());
+ values.put(KEY_LANGUAGE, feed.getLanguage());
+ if (feed.getImage() != null) {
+ if (feed.getImage().getId() == 0) {
+ setImage(feed.getImage());
+ }
+ values.put(KEY_IMAGE, feed.getImage().getId());
+ }
+
+ values.put(KEY_FILE_URL, feed.getFile_url());
+ values.put(KEY_DOWNLOAD_URL, feed.getDownload_url());
+ values.put(KEY_DOWNLOADED, feed.isDownloaded());
+ values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime());
+ values.put(KEY_TYPE, feed.getType());
+ values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier());
+
+ Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong());
+
+ values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong());
+ if (feed.getId() == 0) {
+ // Create new entry
+ if (BuildConfig.DEBUG)
+ Log.d(this.toString(), "Inserting new Feed into db");
+ feed.setId(db.insert(TABLE_NAME_FEEDS, null, values));
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(this.toString(), "Updating existing Feed in db");
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?",
+ new String[]{String.valueOf(feed.getId())});
+
+ }
+ return feed.getId();
+ }
+
+ public void setFeedPreferences(FeedPreferences prefs) {
+ if (prefs.getFeedID() == 0) {
+ throw new IllegalArgumentException("Feed ID of preference must not be null");
+ }
+ ContentValues values = new ContentValues();
+ values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload());
+ values.put(KEY_USERNAME, prefs.getUsername());
+ values.put(KEY_PASSWORD, prefs.getPassword());
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())});
+ }
+
+ /**
+ * Inserts or updates an image entry
+ *
+ * @return the id of the entry
+ */
+ public long setImage(FeedImage image) {
+ db.beginTransaction();
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, image.getTitle());
+ values.put(KEY_DOWNLOAD_URL, image.getDownload_url());
+ values.put(KEY_DOWNLOADED, image.isDownloaded());
+ values.put(KEY_FILE_URL, image.getFile_url());
+ if (image.getId() == 0) {
+ image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?",
+ new String[]{String.valueOf(image.getId())});
+ }
+
+ final FeedComponent owner = image.getOwner();
+ if (owner != null && owner.getId() != 0) {
+ values.clear();
+ values.put(KEY_IMAGE, image.getId());
+ if (owner instanceof Feed) {
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())});
+ }
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ return image.getId();
+ }
+
+ /**
+ * Inserts or updates an image entry
+ *
+ * @return the id of the entry
+ */
+ public long setMedia(FeedMedia media) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_DURATION, media.getDuration());
+ values.put(KEY_POSITION, media.getPosition());
+ values.put(KEY_SIZE, media.getSize());
+ values.put(KEY_MIME_TYPE, media.getMime_type());
+ values.put(KEY_DOWNLOAD_URL, media.getDownload_url());
+ values.put(KEY_DOWNLOADED, media.isDownloaded());
+ values.put(KEY_FILE_URL, media.getFile_url());
+
+ if (media.getPlaybackCompletionDate() != null) {
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, media
+ .getPlaybackCompletionDate().getTime());
+ } else {
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
+ }
+ if (media.getItem() != null) {
+ values.put(KEY_FEEDITEM, media.getItem().getId());
+ }
+ if (media.getId() == 0) {
+ media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ }
+ return media.getId();
+ }
+
+ public void setFeedMediaPlaybackInformation(FeedMedia media) {
+ if (media.getId() != 0) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_POSITION, media.getPosition());
+ values.put(KEY_DURATION, media.getDuration());
+ values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ } else {
+ Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0");
+ }
+ }
+
+ public void setFeedMediaPlaybackCompletionDate(FeedMedia media) {
+ if (media.getId() != 0) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime());
+ values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ } else {
+ Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0");
+ }
+ }
+
+ /**
+ * Insert all FeedItems of a feed and the feed object itself in a single
+ * transaction
+ */
+ public void setCompleteFeed(Feed... feeds) {
+ db.beginTransaction();
+ for (Feed feed : feeds) {
+ setFeed(feed);
+ if (feed.getItems() != null) {
+ for (FeedItem item : feed.getItems()) {
+ setFeedItem(item, false);
+ }
+ }
+ if (feed.getPreferences() != null) {
+ setFeedPreferences(feed.getPreferences());
+ }
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ /**
+ * Update the flattr status of a feed
+ */
+ public void setFeedFlattrStatus(Feed feed) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong());
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())});
+ }
+
+ /**
+ * Get all feeds in the flattr queue.
+ */
+ public Cursor getFeedsInFlattrQueueCursor() {
+ return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?",
+ new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null);
+ }
+
+ /**
+ * Get all feed items in the flattr queue.
+ */
+ public Cursor getFeedItemsInFlattrQueueCursor() {
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?",
+ new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null);
+ }
+
+ /**
+ * Counts feeds and feed items in the flattr queue
+ */
+ public int getFlattrQueueSize() {
+ int res = 0;
+ Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s",
+ TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null);
+ if (c.moveToFirst()) {
+ res = c.getInt(0);
+ c.close();
+ } else {
+ Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds");
+ }
+ c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s",
+ TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null);
+ if (c.moveToFirst()) {
+ res += c.getInt(0);
+ c.close();
+ } else {
+ Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items");
+ }
+
+ return res;
+ }
+
+ /**
+ * Updates the download URL of a Feed.
+ */
+ public void setFeedDownloadUrl(String original, String updated) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_DOWNLOAD_URL, updated);
+ db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original});
+ }
+
+ public void setFeedItemlist(List<FeedItem> items) {
+ db.beginTransaction();
+ for (FeedItem item : items) {
+ setFeedItem(item, true);
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public long setSingleFeedItem(FeedItem item) {
+ db.beginTransaction();
+ long result = setFeedItem(item, true);
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ return result;
+ }
+
+ /**
+ * Update the flattr status of a FeedItem
+ */
+ public void setFeedItemFlattrStatus(FeedItem feedItem) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_FLATTR_STATUS, feedItem.getFlattrStatus().toLong());
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(feedItem.getId())});
+ }
+
+ /**
+ * Update the flattr status of a feed or feed item specified by its payment link
+ * and the new flattr status to use
+ */
+ public void setItemFlattrStatus(String url, FlattrStatus status) {
+ //Log.d(TAG, "setItemFlattrStatus(" + url + ") = " + status.toString());
+ ContentValues values = new ContentValues();
+ values.put(KEY_FLATTR_STATUS, status.toLong());
+
+ // regexps in sqlite would be neat!
+ String[] query_urls = new String[]{
+ "*" + url + "&*",
+ "*" + url + "%2F&*",
+ "*" + url + "",
+ "*" + url + "%2F"
+ };
+
+ if (db.update(TABLE_NAME_FEEDS, values,
+ KEY_PAYMENT_LINK + " GLOB ?"
+ + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
+ + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
+ + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls
+ ) > 0) {
+ Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in Feeds table");
+ return;
+ }
+ if (db.update(TABLE_NAME_FEED_ITEMS, values,
+ KEY_PAYMENT_LINK + " GLOB ?"
+ + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
+ + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
+ + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls
+ ) > 0) {
+ Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in FeedsItems table");
+ }
+ }
+
+ /**
+ * Reset flattr status to unflattrd for all items
+ */
+ public void clearAllFlattrStatus() {
+ ContentValues values = new ContentValues();
+ values.put(KEY_FLATTR_STATUS, 0);
+ db.update(TABLE_NAME_FEEDS, values, null, null);
+ db.update(TABLE_NAME_FEED_ITEMS, values, null, null);
+ }
+
+ /**
+ * Inserts or updates a feeditem entry
+ *
+ * @param item The FeedItem
+ * @param saveFeed true if the Feed of the item should also be saved. This should be set to
+ * false if the method is executed on a list of FeedItems of the same Feed.
+ * @return the id of the entry
+ */
+ private long setFeedItem(FeedItem item, boolean saveFeed) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, item.getTitle());
+ values.put(KEY_LINK, item.getLink());
+ if (item.getDescription() != null) {
+ values.put(KEY_DESCRIPTION, item.getDescription());
+ }
+ if (item.getContentEncoded() != null) {
+ values.put(KEY_CONTENT_ENCODED, item.getContentEncoded());
+ }
+ values.put(KEY_PUBDATE, item.getPubDate().getTime());
+ values.put(KEY_PAYMENT_LINK, item.getPaymentLink());
+ if (saveFeed && item.getFeed() != null) {
+ setFeed(item.getFeed());
+ }
+ values.put(KEY_FEED, item.getFeed().getId());
+ values.put(KEY_READ, item.isRead());
+ values.put(KEY_HAS_CHAPTERS, item.getChapters() != null);
+ values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
+ values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong());
+ if (item.hasItemImage()) {
+ if (item.getImage().getId() == 0) {
+ setImage(item.getImage());
+ }
+ values.put(KEY_IMAGE, item.getImage().getId());
+ }
+
+ if (item.getId() == 0) {
+ item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?",
+ new String[]{String.valueOf(item.getId())});
+ }
+ if (item.getMedia() != null) {
+ setMedia(item.getMedia());
+ }
+ if (item.getChapters() != null) {
+ setChapters(item);
+ }
+ return item.getId();
+ }
+
+ public void setFeedItemRead(boolean read, long itemId, long mediaId,
+ boolean resetMediaPosition) {
+ db.beginTransaction();
+ ContentValues values = new ContentValues();
+
+ values.put(KEY_READ, read);
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)});
+
+ if (resetMediaPosition) {
+ values.clear();
+ values.put(KEY_POSITION, 0);
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(mediaId)});
+ }
+
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public void setFeedItemRead(boolean read, long... itemIds) {
+ db.beginTransaction();
+ ContentValues values = new ContentValues();
+ for (long id : itemIds) {
+ values.clear();
+ values.put(KEY_READ, read);
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(id)});
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public void setChapters(FeedItem item) {
+ ContentValues values = new ContentValues();
+ for (Chapter chapter : item.getChapters()) {
+ values.put(KEY_TITLE, chapter.getTitle());
+ values.put(KEY_START, chapter.getStart());
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_LINK, chapter.getLink());
+ values.put(KEY_CHAPTER_TYPE, chapter.getChapterType());
+ if (chapter.getId() == 0) {
+ chapter.setId(db
+ .insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
+ } else {
+ db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?",
+ new String[]{String.valueOf(chapter.getId())});
+ }
+ }
+ }
+
+ /**
+ * Inserts or updates a download status.
+ */
+ public long setDownloadStatus(DownloadStatus status) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_FEEDFILE, status.getFeedfileId());
+ values.put(KEY_FEEDFILETYPE, status.getFeedfileType());
+ values.put(KEY_REASON, status.getReason().getCode());
+ values.put(KEY_SUCCESSFUL, status.isSuccessful());
+ values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime());
+ values.put(KEY_REASON_DETAILED, status.getReasonDetailed());
+ values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle());
+ if (status.getId() == 0) {
+ status.setId(db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values));
+ } else {
+ db.update(TABLE_NAME_DOWNLOAD_LOG, values, KEY_ID + "=?",
+ new String[]{String.valueOf(status.getId())});
+ }
+ return status.getId();
+ }
+
+ public long getDownloadLogSize() {
+ final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_DOWNLOAD_LOG);
+ Cursor result = db.rawQuery(query, null);
+ long count = 0;
+ if (result.moveToFirst()) {
+ count = result.getLong(0);
+ }
+ result.close();
+ return count;
+ }
+
+ public void removeDownloadLogItems(long count) {
+ if (count > 0) {
+ final String sql = String.format("DELETE FROM %s WHERE %s in (SELECT %s from %s ORDER BY %s ASC LIMIT %d)",
+ TABLE_NAME_DOWNLOAD_LOG, KEY_ID, KEY_ID, TABLE_NAME_DOWNLOAD_LOG, KEY_COMPLETION_DATE, count);
+ db.execSQL(sql, null);
+ }
+ }
+
+ public void setQueue(List<FeedItem> queue) {
+ ContentValues values = new ContentValues();
+ db.beginTransaction();
+ db.delete(TABLE_NAME_QUEUE, null, null);
+ for (int i = 0; i < queue.size(); i++) {
+ FeedItem item = queue.get(i);
+ values.put(KEY_ID, i);
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_FEED, item.getFeed().getId());
+ db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values,
+ SQLiteDatabase.CONFLICT_REPLACE);
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public void clearQueue() {
+ db.delete(TABLE_NAME_QUEUE, null, null);
+ }
+
+ public void removeFeedMedia(FeedMedia media) {
+ db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?",
+ new String[]{String.valueOf(media.getId())});
+ }
+
+ public void removeChaptersOfItem(FeedItem item) {
+ db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?",
+ new String[]{String.valueOf(item.getId())});
+ }
+
+ public void removeFeedImage(FeedImage image) {
+ db.delete(TABLE_NAME_FEED_IMAGES, KEY_ID + "=?",
+ new String[]{String.valueOf(image.getId())});
+ }
+
+ /**
+ * Remove a FeedItem and its FeedMedia entry.
+ */
+ public void removeFeedItem(FeedItem item) {
+ if (item.getMedia() != null) {
+ removeFeedMedia(item.getMedia());
+ }
+ if (item.getChapters() != null) {
+ removeChaptersOfItem(item);
+ }
+ if (item.hasItemImage()) {
+ removeFeedImage(item.getImage());
+ }
+ db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?",
+ new String[]{String.valueOf(item.getId())});
+ }
+
+ /**
+ * Remove a feed with all its FeedItems and Media entries.
+ */
+ public void removeFeed(Feed feed) {
+ db.beginTransaction();
+ if (feed.getImage() != null) {
+ removeFeedImage(feed.getImage());
+ }
+ if (feed.getItems() != null) {
+ for (FeedItem item : feed.getItems()) {
+ removeFeedItem(item);
+ }
+ }
+
+ db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?",
+ new String[]{String.valueOf(feed.getId())});
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public void removeDownloadStatus(DownloadStatus remove) {
+ db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?",
+ new String[]{String.valueOf(remove.getId())});
+ }
+
+ public void clearPlaybackHistory() {
+ ContentValues values = new ContentValues();
+ values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
+ db.update(TABLE_NAME_FEED_MEDIA, values, null, null);
+ }
+
+ /**
+ * Get all Feeds from the Feed Table.
+ *
+ * @return The cursor of the query
+ */
+ public final Cursor getAllFeedsCursor() {
+ Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null,
+ KEY_TITLE + " COLLATE NOCASE ASC");
+ return c;
+ }
+
+ public final Cursor getFeedCursorDownloadUrls() {
+ return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null);
+ }
+
+ public final Cursor getExpiredFeedsCursor(long expirationTime) {
+ Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_LASTUPDATE + " < " + String.valueOf(System.currentTimeMillis() - expirationTime),
+ null, null, null,
+ null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor with all FeedItems of a Feed. Uses FEEDITEM_SEL_FI_SMALL
+ *
+ * @param feed The feed you want to get the FeedItems from.
+ * @return The cursor of the query
+ */
+ public final Cursor getAllItemsOfFeedCursor(final Feed feed) {
+ return getAllItemsOfFeedCursor(feed.getId());
+ }
+
+ public final Cursor getAllItemsOfFeedCursor(final long feedId) {
+ Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
+ + "=?", new String[]{String.valueOf(feedId)}, null, null,
+ null
+ );
+ return c;
+ }
+
+ /**
+ * Return a cursor with the SEL_FI_EXTRA selection of a single feeditem.
+ */
+ public final Cursor getExtraInformationOfItem(final FeedItem item) {
+ Cursor c = db
+ .query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_ID + "=?",
+ new String[]{String.valueOf(item.getId())}, null,
+ null, null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor for a DB query in the FeedMedia table for a given ID.
+ *
+ * @param item The item you want to get the FeedMedia from
+ * @return The cursor of the query
+ */
+ public final Cursor getFeedMediaOfItemCursor(final FeedItem item) {
+ Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?",
+ new String[]{String.valueOf(item.getMedia().getId())}, null,
+ null, null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor for a DB query in the FeedImages table for a given ID.
+ *
+ * @param id ID of the FeedImage
+ * @return The cursor of the query
+ */
+ public final Cursor getImageCursor(final long id) {
+ Cursor c = db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + "=?",
+ new String[]{String.valueOf(id)}, null, null, null);
+ return c;
+ }
+
+ public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) {
+ Cursor c = db.query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_FEEDITEM
+ + "=?", new String[]{String.valueOf(item.getId())}, null,
+ null, null
+ );
+ return c;
+ }
+
+ public final Cursor getDownloadLogCursor(final int limit) {
+ Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null,
+ null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit);
+ return c;
+ }
+
+ /**
+ * Returns a cursor which contains all feed items in the queue. The returned
+ * cursor uses the FEEDITEM_SEL_FI_SMALL selection.
+ */
+ public final Cursor getQueueCursor() {
+ Object[] args = (Object[]) new String[]{
+ SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID,
+ TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE,
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID,
+ TABLE_NAME_QUEUE + "." + KEY_FEEDITEM,
+ TABLE_NAME_QUEUE + "." + KEY_ID};
+ String query = String.format(
+ "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args);
+ Cursor c = db.rawQuery(query, null);
+ /*
+ * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
+ * "INNER JOIN ? ON ?=?", new String[] { TABLE_NAME_QUEUE,
+ * TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." +
+ * KEY_FEEDITEM }, null, null, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM);
+ */
+ return c;
+ }
+
+ public Cursor getQueueIDCursor() {
+ Cursor c = db.query(TABLE_NAME_QUEUE, new String[]{KEY_FEEDITEM}, null, null, null, null, KEY_ID + " ASC", null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor which contains all feed items in the unread items list.
+ * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection.
+ */
+ public final Cursor getUnreadItemsCursor() {
+ Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_READ
+ + "=0", null, null, null, KEY_PUBDATE + " DESC");
+ return c;
+ }
+
+ public final Cursor getUnreadItemIdsCursor() {
+ Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID},
+ KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC");
+ return c;
+
+ }
+
+ public final Cursor getRecentlyPublishedItemsCursor(int limit) {
+ Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, null, null, null, null, KEY_PUBDATE + " DESC LIMIT " + limit);
+ return c;
+ }
+
+ public Cursor getDownloadedItemsCursor() {
+ final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0";
+ Cursor c = db.rawQuery(query, null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor which contains feed media objects with a playback
+ * completion date in ascending order.
+ *
+ * @param limit The maximum row count of the returned cursor. Must be an
+ * integer >= 0.
+ * @throws IllegalArgumentException if limit < 0
+ */
+ public final Cursor getCompletedMediaCursor(int limit) {
+ Validate.isTrue(limit >= 0, "Limit must be >= 0");
+
+ Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null,
+ KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null,
+ null, null);
+ return c;
+ }
+
+ public final Cursor getSingleFeedMediaCursor(long id) {
+ return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null);
+ }
+
+ public final Cursor getFeedMediaCursorByItemID(String... mediaIds) {
+ int length = mediaIds.length;
+ if (length > IN_OPERATOR_MAXIMUM) {
+ Log.w(TAG, "Length of id array is larger than "
+ + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors");
+ int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1;
+ Cursor[] cursors = new Cursor[numCursors];
+ for (int i = 0; i < numCursors; i++) {
+ int neededLength = 0;
+ String[] parts = null;
+ final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM;
+
+ if (elementsLeft >= IN_OPERATOR_MAXIMUM) {
+ neededLength = IN_OPERATOR_MAXIMUM;
+ parts = Arrays.copyOfRange(mediaIds, i
+ * IN_OPERATOR_MAXIMUM, (i + 1)
+ * IN_OPERATOR_MAXIMUM);
+ } else {
+ neededLength = elementsLeft;
+ parts = Arrays.copyOfRange(mediaIds, i
+ * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM)
+ + neededLength);
+ }
+
+ cursors[i] = db.rawQuery("SELECT * FROM "
+ + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_FEEDITEM + " IN "
+ + buildInOperator(neededLength), parts);
+ }
+ return new MergeCursor(cursors);
+ } else {
+ return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN "
+ + buildInOperator(length), mediaIds, null, null, null);
+ }
+ }
+
+ /**
+ * Builds an IN-operator argument depending on the number of items.
+ */
+ private String buildInOperator(int size) {
+ if (size == 1) {
+ return "(?)";
+ }
+ StringBuffer buffer = new StringBuffer("(");
+ for (int i = 0; i < size - 1; i++) {
+ buffer.append("?,");
+ }
+ buffer.append("?)");
+ return buffer.toString();
+ }
+
+ public final Cursor getFeedCursor(final long id) {
+ Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_ID + "=" + id, null,
+ null, null, null);
+ return c;
+ }
+
+ public final Cursor getFeedItemCursor(final String... ids) {
+ if (ids.length > IN_OPERATOR_MAXIMUM) {
+ throw new IllegalArgumentException(
+ "number of IDs must not be larger than "
+ + IN_OPERATOR_MAXIMUM
+ );
+ }
+
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_ID + " IN "
+ + buildInOperator(ids.length), ids, null, null, null);
+
+ }
+
+ public int getQueueSize() {
+ final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
+ Cursor c = db.rawQuery(query, null);
+ int result = 0;
+ if (c.moveToFirst()) {
+ result = c.getInt(0);
+ }
+ c.close();
+ return result;
+ }
+
+ public final int getNumberOfUnreadItems() {
+ final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS +
+ " WHERE " + KEY_READ + " = 0";
+ Cursor c = db.rawQuery(query, null);
+ int result = 0;
+ if (c.moveToFirst()) {
+ result = c.getInt(0);
+ }
+ c.close();
+ return result;
+ }
+
+ public final int getNumberOfDownloadedEpisodes() {
+ final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA +
+ " WHERE " + KEY_DOWNLOADED + " > 0";
+
+ Cursor c = db.rawQuery(query, null);
+ int result = 0;
+ if (c.moveToFirst()) {
+ result = c.getInt(0);
+ }
+ c.close();
+ return result;
+ }
+
+ /**
+ * Uses DatabaseUtils to escape a search query and removes ' at the
+ * beginning and the end of the string returned by the escape method.
+ */
+ private String prepareSearchQuery(String query) {
+ StringBuilder builder = new StringBuilder();
+ DatabaseUtils.appendEscapedSQLString(builder, query);
+ builder.deleteCharAt(0);
+ builder.deleteCharAt(builder.length() - 1);
+ return builder.toString();
+ }
+
+ /**
+ * Searches for the given query in the description of all items or the items
+ * of a specified feed.
+ *
+ * @return A cursor with all search results in SEL_FI_EXTRA selection.
+ */
+ public Cursor searchItemDescriptions(long feedID, String query) {
+ if (feedID != 0) {
+ // search items in specific feed
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
+ + "=? AND " + KEY_DESCRIPTION + " LIKE '%"
+ + prepareSearchQuery(query) + "%'",
+ new String[]{String.valueOf(feedID)}, null, null,
+ null
+ );
+ } else {
+ // search through all items
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
+ KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query)
+ + "%'", null, null, null, null
+ );
+ }
+ }
+
+ /**
+ * Searches for the given query in the content-encoded field of all items or
+ * the items of a specified feed.
+ *
+ * @return A cursor with all search results in SEL_FI_EXTRA selection.
+ */
+ public Cursor searchItemContentEncoded(long feedID, String query) {
+ if (feedID != 0) {
+ // search items in specific feed
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
+ + "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%"
+ + prepareSearchQuery(query) + "%'",
+ new String[]{String.valueOf(feedID)}, null, null,
+ null
+ );
+ } else {
+ // search through all items
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
+ KEY_CONTENT_ENCODED + " LIKE '%"
+ + prepareSearchQuery(query) + "%'", null, null,
+ null, null
+ );
+ }
+ }
+
+ public Cursor searchItemTitles(long feedID, String query) {
+ if (feedID != 0) {
+ // search items in specific feed
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
+ + "=? AND " + KEY_TITLE + " LIKE '%"
+ + prepareSearchQuery(query) + "%'",
+ new String[]{String.valueOf(feedID)}, null, null,
+ null
+ );
+ } else {
+ // search through all items
+ return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
+ KEY_TITLE + " LIKE '%"
+ + prepareSearchQuery(query) + "%'", null, null,
+ null, null
+ );
+ }
+ }
+
+ public Cursor searchItemChapters(long feedID, String searchQuery) {
+ final String query;
+ if (feedID != 0) {
+ query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " +
+ TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" +
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" +
+ feedID + " AND " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%"
+ + prepareSearchQuery(searchQuery) + "%'";
+ } else {
+ query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " +
+ TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" +
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%"
+ + prepareSearchQuery(searchQuery) + "%'";
+ }
+ return db.rawQuery(query, null);
+ }
+
+
+ public static final int IDX_FEEDSTATISTICS_FEED = 0;
+ public static final int IDX_FEEDSTATISTICS_NUM_ITEMS = 1;
+ public static final int IDX_FEEDSTATISTICS_NEW_ITEMS = 2;
+ public static final int IDX_FEEDSTATISTICS_LATEST_EPISODE = 3;
+ public static final int IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES = 4;
+
+ /**
+ * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result
+ * is sorted by the title of the feed.
+ */
+ private static final String FEED_STATISTICS_QUERY = "SELECT Feeds.id, num_items, new_items, latest_episode, in_progress FROM " +
+ " Feeds LEFT JOIN " +
+ "(SELECT feed,count(*) AS num_items," +
+ " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," +
+ " MAX(pubDate) AS latest_episode," +
+ " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," +
+ " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " +
+ " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" +
+ " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;";
+
+ public Cursor getFeedStatisticsCursor() {
+ return db.rawQuery(FEED_STATISTICS_QUERY, null);
+ }
+
+ /**
+ * Helper class for opening the Antennapod database.
+ */
+ private static class PodDBHelper extends SQLiteOpenHelper {
+ /**
+ * Constructor.
+ *
+ * @param context Context to use
+ * @param name Name of the database
+ * @param factory to use for creating cursor objects
+ * @param version number of the database
+ */
+ public PodDBHelper(final Context context, final String name,
+ final CursorFactory factory, final int version) {
+ super(context, name, factory, version);
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_FEEDS);
+ db.execSQL(CREATE_TABLE_FEED_ITEMS);
+ db.execSQL(CREATE_TABLE_FEED_IMAGES);
+ db.execSQL(CREATE_TABLE_FEED_MEDIA);
+ db.execSQL(CREATE_TABLE_DOWNLOAD_LOG);
+ db.execSQL(CREATE_TABLE_QUEUE);
+ db.execSQL(CREATE_TABLE_SIMPLECHAPTERS);
+ }
+
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
+ final int newVersion) {
+ ClientConfig.storageCallbacks.onUpgrade(db, oldVersion, newVersion);
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java
new file mode 100644
index 000000000..9efc5888f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.core.syndication.handler;
+
+import de.danoeh.antennapod.core.feed.Feed;
+import org.apache.commons.io.input.XmlStreamReader;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+
+public class FeedHandler {
+
+ public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException,
+ ParserConfigurationException, UnsupportedFeedtypeException {
+ TypeGetter tg = new TypeGetter();
+ TypeGetter.Type type = tg.getType(feed);
+ SyndHandler handler = new SyndHandler(feed, type);
+
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ SAXParser saxParser = factory.newSAXParser();
+ File file = new File(feed.getFile_url());
+ Reader inputStreamReader = new XmlStreamReader(file);
+ InputSource inputSource = new InputSource(inputStreamReader);
+
+ saxParser.parse(inputSource, handler);
+ inputStreamReader.close();
+ return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java
new file mode 100644
index 000000000..45d1413bf
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java
@@ -0,0 +1,19 @@
+package de.danoeh.antennapod.core.syndication.handler;
+
+import de.danoeh.antennapod.core.feed.Feed;
+
+import java.util.Map;
+
+/**
+ * Container for results returned by the Feed parser
+ */
+public class FeedHandlerResult {
+
+ public Feed feed;
+ public Map<String, String> alternateFeedUrls;
+
+ public FeedHandlerResult(Feed feed, Map<String, String> alternateFeedUrls) {
+ this.feed = feed;
+ this.alternateFeedUrls = alternateFeedUrls;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java
new file mode 100644
index 000000000..413a11f8e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java
@@ -0,0 +1,111 @@
+package de.danoeh.antennapod.core.syndication.handler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Stack;
+
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.syndication.namespace.Namespace;
+import de.danoeh.antennapod.core.syndication.namespace.SyndElement;
+
+/**
+ * Contains all relevant information to describe the current state of a
+ * SyndHandler.
+ */
+public class HandlerState {
+
+ /**
+ * Feed that the Handler is currently processing.
+ */
+ protected Feed feed;
+ /**
+ * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the
+ * URL of the feed, the value is the title
+ */
+ protected Map<String, String> alternateUrls;
+ protected ArrayList<FeedItem> items;
+ protected FeedItem currentItem;
+ protected Stack<SyndElement> tagstack;
+ /**
+ * Namespaces that have been defined so far.
+ */
+ protected HashMap<String, Namespace> namespaces;
+ protected Stack<Namespace> defaultNamespaces;
+ /**
+ * Buffer for saving characters.
+ */
+ protected StringBuffer contentBuf;
+
+ /**
+ * Temporarily saved objects.
+ */
+ protected HashMap<String, Object> tempObjects;
+
+ public HandlerState(Feed feed) {
+ this.feed = feed;
+ alternateUrls = new LinkedHashMap<String, String>();
+ items = new ArrayList<FeedItem>();
+ tagstack = new Stack<SyndElement>();
+ namespaces = new HashMap<String, Namespace>();
+ defaultNamespaces = new Stack<Namespace>();
+ tempObjects = new HashMap<String, Object>();
+ }
+
+ public Feed getFeed() {
+ return feed;
+ }
+
+ public ArrayList<FeedItem> getItems() {
+ return items;
+ }
+
+ public FeedItem getCurrentItem() {
+ return currentItem;
+ }
+
+ public Stack<SyndElement> getTagstack() {
+ return tagstack;
+ }
+
+ public void setFeed(Feed feed) {
+ this.feed = feed;
+ }
+
+ public void setCurrentItem(FeedItem currentItem) {
+ this.currentItem = currentItem;
+ }
+
+ /**
+ * Returns the SyndElement that comes after the top element of the tagstack.
+ */
+ public SyndElement getSecondTag() {
+ SyndElement top = tagstack.pop();
+ SyndElement second = tagstack.peek();
+ tagstack.push(top);
+ return second;
+ }
+
+ public SyndElement getThirdTag() {
+ SyndElement top = tagstack.pop();
+ SyndElement second = tagstack.pop();
+ SyndElement third = tagstack.peek();
+ tagstack.push(second);
+ tagstack.push(top);
+ return third;
+ }
+
+ public StringBuffer getContentBuf() {
+ return contentBuf;
+ }
+
+ public void addAlternateFeedUrl(String title, String url) {
+ alternateUrls.put(url, title);
+ }
+
+ public HashMap<String, Object> getTempObjects() {
+ return tempObjects;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java
new file mode 100644
index 000000000..1dda24944
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java
@@ -0,0 +1,126 @@
+package de.danoeh.antennapod.core.syndication.handler;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.syndication.namespace.*;
+import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/** Superclass for all SAX Handlers which process Syndication formats */
+public class SyndHandler extends DefaultHandler {
+ private static final String TAG = "SyndHandler";
+ private static final String DEFAULT_PREFIX = "";
+ protected HandlerState state;
+
+ public SyndHandler(Feed feed, TypeGetter.Type type) {
+ state = new HandlerState(feed);
+ if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) {
+ state.defaultNamespaces.push(new NSRSS20());
+ }
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ state.contentBuf = new StringBuffer();
+ Namespace handler = getHandlingNamespace(uri, qName);
+ if (handler != null) {
+ SyndElement element = handler.handleElementStart(localName, state,
+ attributes);
+ state.tagstack.push(element);
+
+ }
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length)
+ throws SAXException {
+ if (!state.tagstack.empty()) {
+ if (state.getTagstack().size() >= 2) {
+ if (state.contentBuf != null) {
+ state.contentBuf.append(ch, start, length);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName)
+ throws SAXException {
+ Namespace handler = getHandlingNamespace(uri, qName);
+ if (handler != null) {
+ handler.handleElementEnd(localName, state);
+ state.tagstack.pop();
+
+ }
+ state.contentBuf = null;
+
+ }
+
+ @Override
+ public void endPrefixMapping(String prefix) throws SAXException {
+ if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) {
+ state.defaultNamespaces.pop();
+ }
+ }
+
+ @Override
+ public void startPrefixMapping(String prefix, String uri)
+ throws SAXException {
+ // Find the right namespace
+ if (!state.namespaces.containsKey(uri)) {
+ if (uri.equals(NSAtom.NSURI)) {
+ if (prefix.equals(DEFAULT_PREFIX)) {
+ state.defaultNamespaces.push(new NSAtom());
+ } else if (prefix.equals(NSAtom.NSTAG)) {
+ state.namespaces.put(uri, new NSAtom());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized Atom namespace");
+ }
+ } else if (uri.equals(NSContent.NSURI)
+ && prefix.equals(NSContent.NSTAG)) {
+ state.namespaces.put(uri, new NSContent());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized Content namespace");
+ } else if (uri.equals(NSITunes.NSURI)
+ && prefix.equals(NSITunes.NSTAG)) {
+ state.namespaces.put(uri, new NSITunes());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized ITunes namespace");
+ } else if (uri.equals(NSSimpleChapters.NSURI)
+ && prefix.matches(NSSimpleChapters.NSTAG)) {
+ state.namespaces.put(uri, new NSSimpleChapters());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized SimpleChapters namespace");
+ } else if (uri.equals(NSMedia.NSURI)
+ && prefix.equals(NSMedia.NSTAG)) {
+ state.namespaces.put(uri, new NSMedia());
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized media namespace");
+ }
+ }
+ }
+
+ private Namespace getHandlingNamespace(String uri, String qName) {
+ Namespace handler = state.namespaces.get(uri);
+ if (handler == null && !state.defaultNamespaces.empty()
+ && !qName.contains(":")) {
+ handler = state.defaultNamespaces.peek();
+ }
+ return handler;
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ super.endDocument();
+ state.getFeed().setItems(state.getItems());
+ }
+
+ public HandlerState getState() {
+ return state;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java
new file mode 100644
index 000000000..32cd538d5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java
@@ -0,0 +1,111 @@
+package de.danoeh.antennapod.core.syndication.handler;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Feed;
+import org.apache.commons.io.input.XmlStreamReader;
+import org.jsoup.Jsoup;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.Reader;
+
+/** Gets the type of a specific feed by reading the root element. */
+public class TypeGetter {
+ private static final String TAG = "TypeGetter";
+
+ public enum Type {
+ RSS20, RSS091, ATOM, INVALID
+ }
+
+ private static final String ATOM_ROOT = "feed";
+ private static final String RSS_ROOT = "rss";
+
+ public Type getType(Feed feed) throws UnsupportedFeedtypeException {
+ XmlPullParserFactory factory;
+ if (feed.getFile_url() != null) {
+ try {
+ factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ XmlPullParser xpp = factory.newPullParser();
+ xpp.setInput(createReader(feed));
+ int eventType = xpp.getEventType();
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG) {
+ String tag = xpp.getName();
+ if (tag.equals(ATOM_ROOT)) {
+ feed.setType(Feed.TYPE_ATOM1);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized type Atom");
+ return Type.ATOM;
+ } else if (tag.equals(RSS_ROOT)) {
+ String strVersion = xpp.getAttributeValue(null,
+ "version");
+ if (strVersion != null) {
+
+ if (strVersion.equals("2.0")) {
+ feed.setType(Feed.TYPE_RSS2);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized type RSS 2.0");
+ return Type.RSS20;
+ } else if (strVersion.equals("0.91")
+ || strVersion.equals("0.92")) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Recognized type RSS 0.91/0.92");
+ return Type.RSS091;
+ }
+ }
+ throw new UnsupportedFeedtypeException(Type.INVALID);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Type is invalid");
+ throw new UnsupportedFeedtypeException(Type.INVALID, tag);
+ }
+ } else {
+ eventType = xpp.next();
+ }
+ }
+
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ // XML document might actually be a HTML document -> try to parse as HTML
+ String rootElement = null;
+ try {
+ if (Jsoup.parse(new File(feed.getFile_url()), null) != null) {
+ rootElement = "html";
+ }
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } finally {
+ throw new UnsupportedFeedtypeException(Type.INVALID, rootElement);
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Type is invalid");
+ throw new UnsupportedFeedtypeException(Type.INVALID);
+ }
+
+ private Reader createReader(Feed feed) {
+ Reader reader;
+ try {
+ reader = new XmlStreamReader(new File(feed.getFile_url()));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ return reader;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java
new file mode 100644
index 000000000..3da9251d9
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java
@@ -0,0 +1,38 @@
+package de.danoeh.antennapod.core.syndication.handler;
+
+import de.danoeh.antennapod.core.syndication.handler.TypeGetter.Type;
+
+public class UnsupportedFeedtypeException extends Exception {
+ private static final long serialVersionUID = 9105878964928170669L;
+ private TypeGetter.Type type;
+ private String rootElement;
+
+ public UnsupportedFeedtypeException(Type type) {
+ super();
+ this.type = type;
+ }
+
+ public UnsupportedFeedtypeException(Type type, String rootElement) {
+ this.type = type;
+ this.rootElement = rootElement;
+ }
+
+ public TypeGetter.Type getType() {
+ return type;
+ }
+
+ public String getRootElement() {
+ return rootElement;
+ }
+
+ @Override
+ public String getMessage() {
+ if (type == TypeGetter.Type.INVALID) {
+ return "Invalid type";
+ } else {
+ return "Type " + type + " not supported";
+ }
+ }
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java
new file mode 100644
index 000000000..71bf69ffa
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.core.syndication.namespace;
+
+import de.danoeh.antennapod.core.syndication.handler.HandlerState;
+import org.xml.sax.Attributes;
+
+public class NSContent extends Namespace {
+ public static final String NSTAG = "content";
+ public static final String NSURI = "http://purl.org/rss/1.0/modules/content/";
+
+ private static final String ENCODED = "encoded";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(ENCODED)) {
+ state.getCurrentItem().setContentEncoded(state.getContentBuf().toString());
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java
new file mode 100644
index 000000000..70e1126b9
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java
@@ -0,0 +1,75 @@
+package de.danoeh.antennapod.core.syndication.namespace;
+
+import de.danoeh.antennapod.core.feed.FeedImage;
+import de.danoeh.antennapod.core.syndication.handler.HandlerState;
+import org.xml.sax.Attributes;
+
+import java.util.concurrent.TimeUnit;
+
+public class NSITunes extends Namespace {
+ public static final String NSTAG = "itunes";
+ public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd";
+
+ private static final String IMAGE = "image";
+ private static final String IMAGE_TITLE = "image";
+ private static final String IMAGE_HREF = "href";
+
+ private static final String AUTHOR = "author";
+ public static final String DURATION = "duration";
+
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(IMAGE)) {
+ FeedImage image = new FeedImage();
+ image.setTitle(IMAGE_TITLE);
+ image.setDownload_url(attributes.getValue(IMAGE_HREF));
+
+ if (state.getCurrentItem() != null) {
+ // this is an items image
+ image.setTitle(state.getCurrentItem().getTitle()+IMAGE_TITLE);
+ image.setOwner(state.getCurrentItem());
+ state.getCurrentItem().setImage(image);
+
+ } else {
+ // this is the feed image
+ if (state.getFeed().getImage() == null) {
+ image.setOwner(state.getFeed());
+ state.getFeed().setImage(image);
+ }
+ }
+
+ }
+
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(AUTHOR)) {
+ state.getFeed().setAuthor(state.getContentBuf().toString());
+ } else if (localName.equals(DURATION)) {
+ String[] parts = state.getContentBuf().toString().split(":");
+ try {
+ int duration = 0;
+ if (parts.length == 2) {
+ duration += TimeUnit.MINUTES.toMillis(Long.valueOf(parts[0])) +
+ TimeUnit.SECONDS.toMillis(Long.valueOf(parts[1]));
+ } else if (parts.length >= 3) {
+ duration += TimeUnit.HOURS.toMillis(Long.valueOf(parts[0])) +
+ TimeUnit.MINUTES.toMillis(Long.valueOf(parts[1])) +
+ TimeUnit.SECONDS.toMillis(Long.valueOf(parts[2]));
+ } else {
+ return;
+ }
+
+ state.getTempObjects().put(DURATION, duration);
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
new file mode 100644
index 000000000..7f03f1139
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
@@ -0,0 +1,68 @@
+package de.danoeh.antennapod.core.syndication.namespace;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.syndication.handler.HandlerState;
+import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils;
+import org.xml.sax.Attributes;
+
+import java.util.concurrent.TimeUnit;
+
+/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */
+public class NSMedia extends Namespace {
+ private static final String TAG = "NSMedia";
+
+ public static final String NSTAG = "media";
+ public static final String NSURI = "http://search.yahoo.com/mrss/";
+
+ private static final String CONTENT = "content";
+ private static final String DOWNLOAD_URL = "url";
+ private static final String SIZE = "fileSize";
+ private static final String MIME_TYPE = "type";
+ private static final String DURATION = "duration";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(CONTENT)) {
+ String url = attributes.getValue(DOWNLOAD_URL);
+ String type = attributes.getValue(MIME_TYPE);
+ if (state.getCurrentItem().getMedia() == null
+ && url != null
+ && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils
+ .getValidMimeTypeFromUrl(url)) != null))) {
+
+ long size = 0;
+ try {
+ size = Long.parseLong(attributes.getValue(SIZE));
+ } catch (NumberFormatException e) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Length attribute could not be parsed.");
+ }
+
+ int duration = 0;
+ try {
+ String durationStr = attributes.getValue(DURATION);
+ if (durationStr != null) {
+ duration = (int) TimeUnit.MILLISECONDS.convert(
+ Long.parseLong(durationStr), TimeUnit.SECONDS);
+ }
+ } catch (NumberFormatException e) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Duration attribute could not be parsed");
+ }
+
+ state.getCurrentItem().setMedia(
+ new FeedMedia(state.getCurrentItem(), url, size, type));
+ }
+ }
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
new file mode 100644
index 000000000..0ca261a0e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
@@ -0,0 +1,149 @@
+package de.danoeh.antennapod.core.syndication.namespace;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+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.syndication.handler.HandlerState;
+import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
+import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils;
+import org.xml.sax.Attributes;
+
+/**
+ * SAX-Parser for reading RSS-Feeds
+ *
+ * @author daniel
+ *
+ */
+public class NSRSS20 extends Namespace {
+ private static final String TAG = "NSRSS20";
+ public static final String NSTAG = "rss";
+ public static final String NSURI = "";
+
+ public final static String CHANNEL = "channel";
+ public final static String ITEM = "item";
+ public final static String GUID = "guid";
+ public final static String TITLE = "title";
+ public final static String LINK = "link";
+ public final static String DESCR = "description";
+ public final static String PUBDATE = "pubDate";
+ public final static String ENCLOSURE = "enclosure";
+ public final static String IMAGE = "image";
+ public final static String URL = "url";
+ public final static String LANGUAGE = "language";
+
+ public final static String ENC_URL = "url";
+ public final static String ENC_LEN = "length";
+ public final static String ENC_TYPE = "type";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(ITEM)) {
+ state.setCurrentItem(new FeedItem());
+ state.getItems().add(state.getCurrentItem());
+ state.getCurrentItem().setFeed(state.getFeed());
+
+ } else if (localName.equals(ENCLOSURE)) {
+ String type = attributes.getValue(ENC_TYPE);
+ String url = attributes.getValue(ENC_URL);
+ if (state.getCurrentItem().getMedia() == null
+ && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils
+ .getValidMimeTypeFromUrl(url)) != null))) {
+
+ long size = 0;
+ try {
+ size = Long.parseLong(attributes.getValue(ENC_LEN));
+ } catch (NumberFormatException e) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Length attribute could not be parsed.");
+ }
+ state.getCurrentItem().setMedia(
+ new FeedMedia(state.getCurrentItem(), url, size, type));
+ }
+
+ } else if (localName.equals(IMAGE)) {
+ if (state.getTagstack().size() >= 1) {
+ String parent = state.getTagstack().peek().getName();
+ if (parent.equals(CHANNEL)) {
+ state.getFeed().setImage(new FeedImage());
+ state.getFeed().getImage().setOwner(state.getFeed());
+ }
+ }
+ }
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(ITEM)) {
+ if (state.getCurrentItem() != null) {
+ // the title tag is optional in RSS 2.0. The description is used
+ // as a
+ // title if the item has no title-tag.
+ if (state.getCurrentItem().getTitle() == null) {
+ state.getCurrentItem().setTitle(
+ state.getCurrentItem().getDescription());
+ }
+
+ if (state.getTempObjects().containsKey(NSITunes.DURATION)) {
+ if (state.getCurrentItem().hasMedia()) {
+ state.getCurrentItem().getMedia().setDuration((Integer) state.getTempObjects().get(NSITunes.DURATION));
+ }
+ state.getTempObjects().remove(NSITunes.DURATION);
+ }
+ }
+ state.setCurrentItem(null);
+ } else if (state.getTagstack().size() >= 2
+ && state.getContentBuf() != null) {
+ String content = state.getContentBuf().toString();
+ SyndElement topElement = state.getTagstack().peek();
+ String top = topElement.getName();
+ SyndElement secondElement = state.getSecondTag();
+ String second = secondElement.getName();
+ String third = null;
+ if (state.getTagstack().size() >= 3) {
+ third = state.getThirdTag().getName();
+ }
+
+ if (top.equals(GUID) && second.equals(ITEM)) {
+ // some feed creators include an empty or non-standard guid-element in their feed, which should be ignored
+ if (!content.isEmpty()) {
+ state.getCurrentItem().setItemIdentifier(content);
+ }
+ } else if (top.equals(TITLE)) {
+ if (second.equals(ITEM)) {
+ state.getCurrentItem().setTitle(content);
+ } else if (second.equals(CHANNEL)) {
+ state.getFeed().setTitle(content);
+ } else if (second.equals(IMAGE) && third != null
+ && third.equals(CHANNEL)) {
+ state.getFeed().getImage().setTitle(content);
+ }
+ } else if (top.equals(LINK)) {
+ if (second.equals(CHANNEL)) {
+ state.getFeed().setLink(content);
+ } else if (second.equals(ITEM)) {
+ state.getCurrentItem().setLink(content);
+ }
+ } else if (top.equals(PUBDATE) && second.equals(ITEM)) {
+ state.getCurrentItem().setPubDate(
+ SyndDateUtils.parseRFC822Date(content));
+ } else if (top.equals(URL) && second.equals(IMAGE) && third != null
+ && third.equals(CHANNEL)) {
+ state.getFeed().getImage().setDownload_url(content);
+ } else if (localName.equals(DESCR)) {
+ if (second.equals(CHANNEL)) {
+ state.getFeed().setDescription(content);
+ } else if (second.equals(ITEM)) {
+ state.getCurrentItem().setDescription(content);
+ }
+
+ } else if (localName.equals(LANGUAGE)) {
+ state.getFeed().setLanguage(content.toLowerCase());
+ }
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java
new file mode 100644
index 000000000..a2e5d0187
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java
@@ -0,0 +1,52 @@
+package de.danoeh.antennapod.core.syndication.namespace;
+
+import android.util.Log;
+
+import org.xml.sax.Attributes;
+
+import java.util.ArrayList;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.SimpleChapter;
+import de.danoeh.antennapod.core.syndication.handler.HandlerState;
+import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
+
+public class NSSimpleChapters extends Namespace {
+ private static final String TAG = "NSSimpleChapters";
+
+ public static final String NSTAG = "psc|sc";
+ public static final String NSURI = "http://podlove.org/simple-chapters";
+
+ public static final String CHAPTERS = "chapters";
+ public static final String CHAPTER = "chapter";
+ public static final String START = "start";
+ public static final String TITLE = "title";
+ public static final String HREF = "href";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(CHAPTERS)) {
+ state.getCurrentItem().setChapters(new ArrayList<Chapter>());
+ } else if (localName.equals(CHAPTER)) {
+ try {
+ state.getCurrentItem()
+ .getChapters()
+ .add(new SimpleChapter(SyndDateUtils
+ .parseTimeString(attributes.getValue(START)),
+ attributes.getValue(TITLE), state.getCurrentItem(),
+ attributes.getValue(HREF)));
+ } catch (NumberFormatException e) {
+ if (BuildConfig.DEBUG) Log.w(TAG, "Unable to read chapter", e);
+ }
+ }
+
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java
new file mode 100644
index 000000000..cf118d202
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.core.syndication.namespace;
+
+import de.danoeh.antennapod.core.syndication.handler.HandlerState;
+import org.xml.sax.Attributes;
+
+
+public abstract class Namespace {
+ public static final String NSTAG = null;
+ public static final String NSURI = null;
+
+ /** Called by a Feedhandler when in startElement and it detects a namespace element
+ * @return The SyndElement to push onto the stack
+ * */
+ public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes);
+
+ /** Called by a Feedhandler when in endElement and it detects a namespace element
+ * @return true if namespace handled the element, false if it ignored it
+ * */
+ public abstract void handleElementEnd(String localName, HandlerState state);
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java
new file mode 100644
index 000000000..8adcd2086
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java
@@ -0,0 +1,22 @@
+package de.danoeh.antennapod.core.syndication.namespace;
+
+/** Defines a XML Element that is pushed on the tagstack */
+public class SyndElement {
+ protected String name;
+ protected Namespace namespace;
+
+ public SyndElement(String name, Namespace namespace) {
+ this.name = name;
+ this.namespace = namespace;
+ }
+
+ public Namespace getNamespace() {
+ return namespace;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java
new file mode 100644
index 000000000..43fe0edb7
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.core.syndication.namespace.atom;
+
+import de.danoeh.antennapod.core.syndication.namespace.Namespace;
+import de.danoeh.antennapod.core.syndication.namespace.SyndElement;
+import org.apache.commons.lang3.StringEscapeUtils;
+
+/** Represents Atom Element which contains text (content, title, summary). */
+public class AtomText extends SyndElement {
+ public static final String TYPE_TEXT = "text";
+ public static final String TYPE_HTML = "html";
+ public static final String TYPE_XHTML = "xhtml";
+
+ private String type;
+ private String content;
+
+ public AtomText(String name, Namespace namespace, String type) {
+ super(name, namespace);
+ this.type = type;
+ }
+
+ /** Processes the content according to the type and returns it. */
+ public String getProcessedContent() {
+ if (type == null) {
+ return content;
+ } else if (type.equals(TYPE_HTML)) {
+ return StringEscapeUtils.unescapeHtml4(content);
+ } else if (type.equals(TYPE_XHTML)) {
+ return content;
+ } else { // Handle as text by default
+ return content;
+ }
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
new file mode 100644
index 000000000..1de001c55
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
@@ -0,0 +1,202 @@
+package de.danoeh.antennapod.core.syndication.namespace.atom;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+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.syndication.handler.HandlerState;
+import de.danoeh.antennapod.core.syndication.namespace.NSITunes;
+import de.danoeh.antennapod.core.syndication.namespace.NSRSS20;
+import de.danoeh.antennapod.core.syndication.namespace.Namespace;
+import de.danoeh.antennapod.core.syndication.namespace.SyndElement;
+import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
+import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils;
+import org.xml.sax.Attributes;
+
+public class NSAtom extends Namespace {
+ private static final String TAG = "NSAtom";
+ public static final String NSTAG = "atom";
+ public static final String NSURI = "http://www.w3.org/2005/Atom";
+
+ private static final String FEED = "feed";
+ private static final String ID = "id";
+ private static final String TITLE = "title";
+ private static final String ENTRY = "entry";
+ private static final String LINK = "link";
+ private static final String UPDATED = "updated";
+ private static final String AUTHOR = "author";
+ private static final String CONTENT = "content";
+ private static final String IMAGE = "logo";
+ private static final String SUBTITLE = "subtitle";
+ private static final String PUBLISHED = "published";
+
+ private static final String TEXT_TYPE = "type";
+ // Link
+ private static final String LINK_HREF = "href";
+ private static final String LINK_REL = "rel";
+ private static final String LINK_TYPE = "type";
+ private static final String LINK_TITLE = "title";
+ private static final String LINK_LENGTH = "length";
+ // rel-values
+ private static final String LINK_REL_ALTERNATE = "alternate";
+ private static final String LINK_REL_ENCLOSURE = "enclosure";
+ private static final String LINK_REL_PAYMENT = "payment";
+ private static final String LINK_REL_RELATED = "related";
+ private static final String LINK_REL_SELF = "self";
+ // type-values
+ private static final String LINK_TYPE_ATOM = "application/atom+xml";
+ private static final String LINK_TYPE_HTML = "text/html";
+ private static final String LINK_TYPE_XHTML = "application/xml+xhtml";
+
+ private static final String LINK_TYPE_RSS = "application/rss+xml";
+
+ /**
+ * Regexp to test whether an Element is a Text Element.
+ */
+ private static final String isText = TITLE + "|" + CONTENT + "|" + "|"
+ + SUBTITLE;
+
+ public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL;
+ public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM;
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(ENTRY)) {
+ state.setCurrentItem(new FeedItem());
+ state.getItems().add(state.getCurrentItem());
+ state.getCurrentItem().setFeed(state.getFeed());
+ } else if (localName.matches(isText)) {
+ String type = attributes.getValue(TEXT_TYPE);
+ return new AtomText(localName, this, type);
+ } else if (localName.equals(LINK)) {
+ String href = attributes.getValue(LINK_HREF);
+ String rel = attributes.getValue(LINK_REL);
+ SyndElement parent = state.getTagstack().peek();
+ if (parent.getName().matches(isFeedItem)) {
+ if (rel == null || rel.equals(LINK_REL_ALTERNATE)) {
+ state.getCurrentItem().setLink(href);
+ } else if (rel.equals(LINK_REL_ENCLOSURE)) {
+ String strSize = attributes.getValue(LINK_LENGTH);
+ long size = 0;
+ try {
+ if (strSize != null) {
+ size = Long.parseLong(strSize);
+ }
+ } catch (NumberFormatException e) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Length attribute could not be parsed.");
+ }
+ String type = attributes.getValue(LINK_TYPE);
+ if (SyndTypeUtils.enclosureTypeValid(type)
+ || (type = SyndTypeUtils
+ .getValidMimeTypeFromUrl(href)) != null) {
+ state.getCurrentItem().setMedia(
+ new FeedMedia(state.getCurrentItem(), href,
+ size, type)
+ );
+ }
+ } else if (rel.equals(LINK_REL_PAYMENT)) {
+ state.getCurrentItem().setPaymentLink(href);
+ }
+ } else if (parent.getName().matches(isFeed)) {
+ if (rel == null || rel.equals(LINK_REL_ALTERNATE)) {
+ String type = attributes.getValue(LINK_TYPE);
+ /*
+ * Use as link if a) no type-attribute is given and
+ * feed-object has no link yet b) type of link is
+ * LINK_TYPE_HTML or LINK_TYPE_XHTML
+ */
+ if ((type == null && state.getFeed().getLink() == null)
+ || (type != null && (type.equals(LINK_TYPE_HTML) || type.equals(LINK_TYPE_XHTML)))) {
+ state.getFeed().setLink(href);
+ } else if (type != null && (type.equals(LINK_TYPE_ATOM) || type.equals(LINK_TYPE_RSS))) {
+ // treat as podlove alternate feed
+ String title = attributes.getValue(LINK_TITLE);
+ if (title == null) {
+ title = href;
+ }
+ state.addAlternateFeedUrl(title, href);
+ }
+ } else if (rel.equals(LINK_REL_PAYMENT)) {
+ state.getFeed().setPaymentLink(href);
+ }
+ }
+ }
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(ENTRY)) {
+ if (state.getCurrentItem() != null &&
+ state.getTempObjects().containsKey(NSITunes.DURATION)) {
+ if (state.getCurrentItem().hasMedia()) {
+ state.getCurrentItem().getMedia().setDuration((Integer) state.getTempObjects().get(NSITunes.DURATION));
+ }
+ state.getTempObjects().remove(NSITunes.DURATION);
+ }
+ state.setCurrentItem(null);
+ }
+
+ if (state.getTagstack().size() >= 2) {
+ AtomText textElement = null;
+ String content;
+ if (state.getContentBuf() != null) {
+ content = state.getContentBuf().toString();
+ } else {
+ content = "";
+ }
+ SyndElement topElement = state.getTagstack().peek();
+ String top = topElement.getName();
+ SyndElement secondElement = state.getSecondTag();
+ String second = secondElement.getName();
+
+ if (top.matches(isText)) {
+ textElement = (AtomText) topElement;
+ textElement.setContent(content);
+ }
+
+ if (top.equals(ID)) {
+ if (second.equals(FEED)) {
+ state.getFeed().setFeedIdentifier(content);
+ } else if (second.equals(ENTRY)) {
+ state.getCurrentItem().setItemIdentifier(content);
+ }
+ } else if (top.equals(TITLE)) {
+
+ if (second.equals(FEED)) {
+ state.getFeed().setTitle(textElement.getProcessedContent());
+ } else if (second.equals(ENTRY)) {
+ state.getCurrentItem().setTitle(
+ textElement.getProcessedContent());
+ }
+ } else if (top.equals(SUBTITLE)) {
+ if (second.equals(FEED)) {
+ state.getFeed().setDescription(
+ textElement.getProcessedContent());
+ }
+ } else if (top.equals(CONTENT)) {
+ if (second.equals(ENTRY)) {
+ state.getCurrentItem().setDescription(
+ textElement.getProcessedContent());
+ }
+ } else if (top.equals(UPDATED)) {
+ if (second.equals(ENTRY)
+ && state.getCurrentItem().getPubDate() == null) {
+ state.getCurrentItem().setPubDate(
+ SyndDateUtils.parseRFC3339Date(content));
+ }
+ } else if (top.equals(PUBLISHED)) {
+ if (second.equals(ENTRY)) {
+ state.getCurrentItem().setPubDate(
+ SyndDateUtils.parseRFC3339Date(content));
+ }
+ } else if (top.equals(IMAGE)) {
+ state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));
+ }
+
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java
new file mode 100644
index 000000000..d493286ac
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java
@@ -0,0 +1,161 @@
+package de.danoeh.antennapod.core.syndication.util;
+
+import android.util.Log;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Parses several date formats.
+ */
+public class SyndDateUtils {
+ private static final String TAG = "DateUtils";
+
+ private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z",};
+
+ /**
+ * RFC 3339 date format for UTC dates.
+ */
+ public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+ /**
+ * RFC 3339 date format for localtime dates with offset.
+ */
+ public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ";
+
+ private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ protected SimpleDateFormat initialValue() {
+ return new SimpleDateFormat(RFC822DATES[0], Locale.US);
+ }
+
+ };
+
+ private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ protected SimpleDateFormat initialValue() {
+ return new SimpleDateFormat(RFC3339UTC, Locale.US);
+ }
+
+ };
+
+ public static Date parseRFC822Date(String date) {
+ Date result = null;
+ if (date.contains("PDT")) {
+ date = date.replace("PDT", "PST8PDT");
+ }
+ if (date.contains(",")) {
+ // Remove day of the week
+ date = date.substring(date.indexOf(",") + 1).trim();
+ }
+ SimpleDateFormat format = RFC822Formatter.get();
+ for (int i = 0; i < RFC822DATES.length; i++) {
+ try {
+ format.applyPattern(RFC822DATES[i]);
+ result = format.parse(date);
+ break;
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ }
+ if (result == null) {
+ Log.e(TAG, "Unable to parse feed date correctly");
+ }
+
+ return result;
+ }
+
+ public static Date parseRFC3339Date(String date) {
+ Date result = null;
+ SimpleDateFormat format = RFC3339Formatter.get();
+ boolean isLocal = date.endsWith("Z");
+ if (date.contains(".")) {
+ // remove secfrac
+ int fracIndex = date.indexOf(".");
+ String first = date.substring(0, fracIndex);
+ String second = null;
+ if (isLocal) {
+ second = date.substring(date.length() - 1);
+ } else {
+ if (date.contains("+")) {
+ second = date.substring(date.indexOf("+"));
+ } else {
+ second = date.substring(date.indexOf("-"));
+ }
+ }
+
+ date = first + second;
+ }
+ if (isLocal) {
+ try {
+ result = format.parse(date);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ } else {
+ format.applyPattern(RFC3339LOCAL);
+ // remove last colon
+ StringBuffer buf = new StringBuffer(date.length() - 1);
+ int colonIdx = date.lastIndexOf(':');
+ for (int x = 0; x < date.length(); x++) {
+ if (x != colonIdx)
+ buf.append(date.charAt(x));
+ }
+ String bufStr = buf.toString();
+ try {
+ result = format.parse(bufStr);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ Log.e(TAG, "Unable to parse date");
+ } finally {
+ format.applyPattern(RFC3339UTC);
+ }
+
+ }
+
+ return result;
+
+ }
+
+ /**
+ * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to
+ * milliseconds.
+ *
+ * @throws java.lang.NumberFormatException if the number segments contain invalid numbers.
+ */
+ public static long parseTimeString(final String time) {
+ String[] parts = time.split(":");
+ long result = 0;
+ int idx = 0;
+ if (parts.length == 3) {
+ // string has hours
+ result += Integer.valueOf(parts[idx]) * 3600000L;
+ idx++;
+ }
+ result += Integer.valueOf(parts[idx]) * 60000L;
+ idx++;
+ result += (Float.valueOf(parts[idx])) * 1000L;
+ return result;
+ }
+
+ public static String formatRFC822Date(Date date) {
+ SimpleDateFormat format = RFC822Formatter.get();
+ return format.format(date);
+ }
+
+ public static String formatRFC3339Local(Date date) {
+ SimpleDateFormat format = RFC3339Formatter.get();
+ format.applyPattern(RFC3339LOCAL);
+ String result = format.format(date);
+ format.applyPattern(RFC3339UTC);
+ return result;
+ }
+
+ public static String formatRFC3339UTC(Date date) {
+ SimpleDateFormat format = RFC3339Formatter.get();
+ format.applyPattern(RFC3339UTC);
+ return format.format(date);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java
new file mode 100644
index 000000000..8d1d8ffde
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.core.syndication.util;
+
+import android.webkit.MimeTypeMap;
+import org.apache.commons.io.FilenameUtils;
+
+/** Utility class for handling MIME-Types of enclosures */
+public class SyndTypeUtils {
+
+ private final static String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*"
+ + "|" + "application/ogg";
+
+ private SyndTypeUtils() {
+
+ }
+
+ public static boolean enclosureTypeValid(String type) {
+ if (type == null) {
+ return false;
+ } else {
+ return type.matches(VALID_MIMETYPE);
+ }
+ }
+
+ /**
+ * Should be used if mime-type of enclosure tag is not supported. This
+ * method will check if the mime-type of the file extension is supported. If
+ * the type is not supported, this method will return null.
+ */
+ public static String getValidMimeTypeFromUrl(String url) {
+ if (url != null) {
+ String extension = FilenameUtils.getExtension(url);
+ if (extension != null) {
+ String type = MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(extension);
+ if (type != null && enclosureTypeValid(type)) {
+ return type;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
new file mode 100644
index 000000000..8dd9ffe4b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
@@ -0,0 +1,274 @@
+package de.danoeh.antennapod.core.util;
+
+import android.util.Log;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator;
+import de.danoeh.antennapod.core.util.id3reader.ChapterReader;
+import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentChapterReader;
+import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException;
+
+/**
+ * Utility class for getting chapter data from media files.
+ */
+public class ChapterUtils {
+ private static final String TAG = "ChapterUtils";
+
+ private ChapterUtils() {
+ }
+
+ /**
+ * Uses the download URL of a media object of a feeditem to read its ID3
+ * chapters.
+ */
+ public static void readID3ChaptersFromPlayableStreamUrl(Playable p) {
+ if (p != null && p.getStreamUrl() != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
+ InputStream in = null;
+ try {
+ URL url = new URL(p.getStreamUrl());
+ ChapterReader reader = new ChapterReader();
+
+ in = url.openStream();
+ reader.readInputStream(in);
+ List<Chapter> chapters = reader.getChapters();
+
+ if (chapters != null) {
+ Collections
+ .sort(chapters, new ChapterStartTimeComparator());
+ processChapters(chapters, p);
+ if (chaptersValid(chapters)) {
+ p.setChapters(chapters);
+ Log.i(TAG, "Chapters loaded");
+ } else {
+ Log.e(TAG, "Chapter data was invalid");
+ }
+ } else {
+ Log.i(TAG, "ChapterReader could not find any ID3 chapters");
+ }
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (ID3ReaderException e) {
+ e.printStackTrace();
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ } else {
+ Log.e(TAG,
+ "Unable to read ID3 chapters: media or download URL was null");
+ }
+ }
+
+ /**
+ * Uses the file URL of a media object of a feeditem to read its ID3
+ * chapters.
+ */
+ public static void readID3ChaptersFromPlayableFileUrl(Playable p) {
+ if (p != null && p.localFileAvailable() && p.getLocalMediaUrl() != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
+ File source = new File(p.getLocalMediaUrl());
+ if (source.exists()) {
+ ChapterReader reader = new ChapterReader();
+ InputStream in = null;
+
+ try {
+ in = new BufferedInputStream(new FileInputStream(source));
+ reader.readInputStream(in);
+ List<Chapter> chapters = reader.getChapters();
+
+ if (chapters != null) {
+ Collections.sort(chapters,
+ new ChapterStartTimeComparator());
+ processChapters(chapters, p);
+ if (chaptersValid(chapters)) {
+ p.setChapters(chapters);
+ Log.i(TAG, "Chapters loaded");
+ } else {
+ Log.e(TAG, "Chapter data was invalid");
+ }
+ } else {
+ Log.i(TAG,
+ "ChapterReader could not find any ID3 chapters");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (ID3ReaderException e) {
+ e.printStackTrace();
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ } else {
+ Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist");
+ }
+ }
+ }
+
+ public static void readOggChaptersFromPlayableStreamUrl(Playable media) {
+ if (media != null && media.streamAvailable()) {
+ InputStream input = null;
+ try {
+ URL url = new URL(media.getStreamUrl());
+ input = url.openStream();
+ if (input != null) {
+ readOggChaptersFromInputStream(media, input);
+ }
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ IOUtils.closeQuietly(input);
+ }
+ }
+ }
+
+ public static void readOggChaptersFromPlayableFileUrl(Playable media) {
+ if (media != null && media.getLocalMediaUrl() != null) {
+ File source = new File(media.getLocalMediaUrl());
+ if (source.exists()) {
+ InputStream input = null;
+ try {
+ input = new BufferedInputStream(new FileInputStream(source));
+ readOggChaptersFromInputStream(media, input);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } finally {
+ IOUtils.closeQuietly(input);
+ }
+ }
+ }
+ }
+
+ private static void readOggChaptersFromInputStream(Playable p,
+ InputStream input) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Trying to read chapters from item with title "
+ + p.getEpisodeTitle());
+ try {
+ VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
+ reader.readInputStream(input);
+ List<Chapter> chapters = reader.getChapters();
+ if (chapters != null) {
+ Collections.sort(chapters, new ChapterStartTimeComparator());
+ processChapters(chapters, p);
+ if (chaptersValid(chapters)) {
+ p.setChapters(chapters);
+ Log.i(TAG, "Chapters loaded");
+ } else {
+ Log.e(TAG, "Chapter data was invalid");
+ }
+ } else {
+ Log.i(TAG,
+ "ChapterReader could not find any Ogg vorbis chapters");
+ }
+ } catch (VorbisCommentReaderException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Makes sure that chapter does a title and an item attribute.
+ */
+ private static void processChapters(List<Chapter> chapters, Playable p) {
+ for (int i = 0; i < chapters.size(); i++) {
+ Chapter c = chapters.get(i);
+ if (c.getTitle() == null) {
+ c.setTitle(Integer.toString(i));
+ }
+ }
+ }
+
+ private static boolean chaptersValid(List<Chapter> chapters) {
+ if (chapters.isEmpty()) {
+ return false;
+ }
+ for (Chapter c : chapters) {
+ if (c.getTitle() == null) {
+ return false;
+ }
+ if (c.getStart() < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Calls getCurrentChapter with current position.
+ */
+ public static Chapter getCurrentChapter(Playable media) {
+ if (media.getChapters() != null) {
+ List<Chapter> chapters = media.getChapters();
+ Chapter current = null;
+ if (chapters != null) {
+ current = chapters.get(0);
+ for (Chapter sc : chapters) {
+ if (sc.getStart() > media.getPosition()) {
+ break;
+ } else {
+ current = sc;
+ }
+ }
+ }
+ return current;
+ } else {
+ return null;
+ }
+ }
+
+ public static void loadChaptersFromStreamUrl(Playable media) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting chapterLoader thread");
+ ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media);
+ if (media.getChapters() == null) {
+ ChapterUtils.readOggChaptersFromPlayableStreamUrl(media);
+ }
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "ChapterLoaderThread has finished");
+ }
+
+ public static void loadChaptersFromFileUrl(Playable media) {
+ if (media.localFileAvailable()) {
+ ChapterUtils.readID3ChaptersFromPlayableFileUrl(media);
+ if (media.getChapters() == null) {
+ ChapterUtils.readOggChaptersFromPlayableFileUrl(media);
+ }
+ } else {
+ Log.e(TAG, "Could not load chapters from file url: local file not available");
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java
new file mode 100644
index 000000000..a0b514bd6
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java
@@ -0,0 +1,103 @@
+package de.danoeh.antennapod.core.util;
+
+import android.util.Log;
+
+/** Provides methods for converting various units. */
+public final class Converter {
+ /** Class shall not be instantiated. */
+ private Converter() {
+ }
+
+ /** Logging tag. */
+ private static final String TAG = "Converter";
+
+
+ /** Indicates that the value is in the Byte range.*/
+ private static final int B_RANGE = 0;
+ /** Indicates that the value is in the Kilobyte range.*/
+ private static final int KB_RANGE = 1;
+ /** Indicates that the value is in the Megabyte range.*/
+ private static final int MB_RANGE = 2;
+ /** Indicates that the value is in the Gigabyte range.*/
+ private static final int GB_RANGE = 3;
+ /** Determines the length of the number for best readability.*/
+ private static final int NUM_LENGTH = 1024;
+
+
+ private static final int HOURS_MIL = 3600000;
+ private static final int MINUTES_MIL = 60000;
+ private static final int SECONDS_MIL = 1000;
+
+ /** Takes a byte-value and converts it into a more readable
+ * String.
+ * @param input The value to convert
+ * @return The converted String with a unit
+ * */
+ public static String byteToString(final long input) {
+ int i = 0;
+ int result = 0;
+
+ for (i = 0; i < GB_RANGE + 1; i++) {
+ result = (int) (input / Math.pow(1024, i));
+ if (result < NUM_LENGTH) {
+ break;
+ }
+ }
+
+ switch (i) {
+ case B_RANGE:
+ return result + " B";
+ case KB_RANGE:
+ return result + " KB";
+ case MB_RANGE:
+ return result + " MB";
+ case GB_RANGE:
+ return result + " GB";
+ default:
+ Log.e(TAG, "Error happened in byteToString");
+ return "ERROR";
+ }
+ }
+
+ /** Converts milliseconds to a string containing hours, minutes and seconds */
+ public static String getDurationStringLong(int duration) {
+ int h = duration / HOURS_MIL;
+ int rest = duration - h * HOURS_MIL;
+ int m = rest / MINUTES_MIL;
+ rest -= m * MINUTES_MIL;
+ int s = rest / SECONDS_MIL;
+
+ return String.format("%02d:%02d:%02d", h, m, s);
+ }
+
+ /** Converts milliseconds to a string containing hours and minutes */
+ public static String getDurationStringShort(int duration) {
+ int h = duration / HOURS_MIL;
+ int rest = duration - h * HOURS_MIL;
+ int m = rest / MINUTES_MIL;
+
+ return String.format("%02d:%02d", h, m);
+ }
+
+ /** Converts long duration string (HH:MM:SS) to milliseconds. */
+ public static int durationStringLongToMs(String input) {
+ String[] parts = input.split(":");
+ if (parts.length != 3) {
+ return 0;
+ }
+ return Integer.valueOf(parts[0]) * 3600 * 1000 +
+ Integer.valueOf(parts[1]) * 60 * 1000 +
+ Integer.valueOf(parts[2]) * 1000;
+ }
+
+ /** Converts short duration string (HH:MM) to milliseconds. */
+ public static int durationStringShortToMs(String input) {
+ String[] parts = input.split(":");
+ if (parts.length != 2) {
+ return 0;
+ }
+ return Integer.valueOf(parts[0]) * 3600 * 1000 +
+ Integer.valueOf(parts[1]) * 1000 * 60;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java
new file mode 100644
index 000000000..602c221bf
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java
@@ -0,0 +1,52 @@
+package de.danoeh.antennapod.core.util;
+
+import android.content.Context;
+import de.danoeh.antennapod.core.R;
+
+/** Utility class for Download Errors. */
+public enum DownloadError {
+ SUCCESS(0, R.string.download_successful),
+ ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception),
+ ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type),
+ ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error),
+ ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown),
+ ERROR_IO_ERROR(5, R.string.download_error_io_error),
+ ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown),
+ ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown),
+ ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found),
+ ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error),
+ ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space),
+ ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host),
+ ERROR_REQUEST_ERROR(12, R.string.download_error_request_error),
+ ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access),
+ ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized);
+
+ private final int code;
+ private final int resId;
+
+ private DownloadError(int code, int resId) {
+ this.code = code;
+ this.resId = resId;
+ }
+
+ /** Return DownloadError from its associated code. */
+ public static DownloadError fromCode(int code) {
+ for (DownloadError reason : values()) {
+ if (reason.getCode() == code) {
+ return reason;
+ }
+ }
+ throw new IllegalArgumentException("unknown code: " + code);
+ }
+
+ /** Get machine-readable code. */
+ public int getCode() {
+ return code;
+ }
+
+ /** Get a human-readable string. */
+ public String getErrorString(Context context) {
+ return context.getString(resId);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java
new file mode 100644
index 000000000..f432424f8
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java
@@ -0,0 +1,117 @@
+/* Adapted from: http://thinking-in-code.blogspot.com/2008/11/duck-typing-in-java-using-dynamic.html */
+
+package de.danoeh.antennapod.core.util;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import de.danoeh.antennapod.core.BuildConfig;
+
+/**
+ * Allows "duck typing" or dynamic invocation based on method signature rather
+ * than type hierarchy. In other words, rather than checking whether something
+ * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck.
+ *
+ * To use first use the coerce static method to indicate the object you want to
+ * do Duck Typing for, then specify an interface to the to method which you want
+ * to coerce the type to, e.g:
+ *
+ * public interface Foo { void aMethod(); } class Bar { ... public void
+ * aMethod() { ... } ... } Bar bar = ...; Foo foo =
+ * DuckType.coerce(bar).to(Foo.class); foo.aMethod();
+ *
+ *
+ */
+public class DuckType {
+
+ private final Object objectToCoerce;
+
+ private DuckType(Object objectToCoerce) {
+ this.objectToCoerce = objectToCoerce;
+ }
+
+ private class CoercedProxy implements InvocationHandler {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ Method delegateMethod = findMethodBySignature(method);
+ assert delegateMethod != null;
+ return delegateMethod.invoke(DuckType.this.objectToCoerce, args);
+ }
+ }
+
+ /**
+ * Specify the duck typed object to coerce.
+ *
+ * @param object
+ * the object to coerce
+ * @return
+ */
+ public static DuckType coerce(Object object) {
+ return new DuckType(object);
+ }
+
+ /**
+ * Coerce the Duck Typed object to the given interface providing it
+ * implements all the necessary methods.
+ *
+ * @param
+ * @param iface
+ * @return an instance of the given interface that wraps the duck typed
+ * class
+ * @throws ClassCastException
+ * if the object being coerced does not implement all the
+ * methods in the given interface.
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public <T> T to(Class iface) {
+ if (BuildConfig.DEBUG && !iface.isInterface()) throw new AssertionError("cannot coerce object to a class, must be an interface");
+ if (isA(iface)) {
+ return (T) iface.cast(objectToCoerce);
+ }
+ if (quacksLikeA(iface)) {
+ return generateProxy(iface);
+ }
+ throw new ClassCastException("Could not coerce object of type " + objectToCoerce.getClass() + " to " + iface);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private boolean isA(Class iface) {
+ return objectToCoerce.getClass().isInstance(iface);
+ }
+
+ /**
+ * Determine whether the duck typed object can be used with the given
+ * interface.
+ *
+ * @param Type
+ * of the interface to check.
+ * @param iface
+ * Interface class to check
+ * @return true if the object will support all the methods in the interface,
+ * false otherwise.
+ */
+ @SuppressWarnings("rawtypes")
+ public boolean quacksLikeA(Class iface) {
+ for (Method method : iface.getMethods()) {
+ if (findMethodBySignature(method) == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private <T> T generateProxy(Class iface) {
+ return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new CoercedProxy());
+ }
+
+ private Method findMethodBySignature(Method method) {
+ try {
+ return objectToCoerce.getClass().getMethod(method.getName(), method.getParameterTypes());
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java
new file mode 100644
index 000000000..4c23b161b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java
@@ -0,0 +1,49 @@
+package de.danoeh.antennapod.core.util;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EpisodeFilter {
+ private EpisodeFilter() {
+
+ }
+
+ /** Return a copy of the itemlist without items which have no media. */
+ public static ArrayList<FeedItem> getEpisodeList(List<FeedItem> items) {
+ ArrayList<FeedItem> episodes = new ArrayList<FeedItem>(items);
+ for (FeedItem item : items) {
+ if (item.getMedia() == null) {
+ episodes.remove(item);
+ }
+ }
+ return episodes;
+ }
+
+ public static int countItemsWithEpisodes(List<FeedItem> items) {
+ int count = 0;
+ for (FeedItem item : items) {
+ if (item.getMedia() != null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public static FeedItem accessEpisodeByIndex(List<FeedItem> items,
+ int position) {
+ int count = 0;
+ for (FeedItem item : items) {
+
+ if (item.getMedia() != null) {
+ if (count == position) {
+ return item;
+ } else {
+ count++;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java
new file mode 100644
index 000000000..bf14cd23e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.core.util;
+
+import de.danoeh.antennapod.core.feed.Feed;
+
+import java.util.Comparator;
+
+/** Compares the title of two feeds for sorting. */
+public class FeedtitleComparator implements Comparator<Feed> {
+
+ @Override
+ public int compare(Feed lhs, Feed rhs) {
+ return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java
new file mode 100644
index 000000000..00c023b64
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java
@@ -0,0 +1,36 @@
+package de.danoeh.antennapod.core.util;
+
+import java.util.Arrays;
+
+/** Generates valid filenames for a given string. */
+public class FileNameGenerator {
+
+ private static final char[] ILLEGAL_CHARACTERS = { '/', '\\', '?', '%',
+ '*', ':', '|', '"', '<', '>' };
+ static {
+ Arrays.sort(ILLEGAL_CHARACTERS);
+ }
+
+ private FileNameGenerator() {
+
+ }
+
+ /**
+ * This method will return a new string that doesn't contain any illegal
+ * characters of the given string.
+ */
+ public static String generateFileName(String string) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < string.length(); i++) {
+ char c = string.charAt(i);
+ if (Arrays.binarySearch(ILLEGAL_CHARACTERS, c) < 0) {
+ builder.append(c);
+ }
+ }
+ return builder.toString().replaceFirst(" *$","");
+ }
+
+ public static long generateLong(final String str) {
+ return str.hashCode();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java b/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java
new file mode 100644
index 000000000..c98c2d82a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java
@@ -0,0 +1,21 @@
+package de.danoeh.antennapod.core.util;
+
+/** Thrown if a feed has invalid attribute values. */
+public class InvalidFeedException extends Exception {
+
+ public InvalidFeedException() {
+ }
+
+ public InvalidFeedException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public InvalidFeedException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public InvalidFeedException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java
new file mode 100644
index 000000000..07432d28a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java
@@ -0,0 +1,120 @@
+package de.danoeh.antennapod.core.util;
+
+import java.nio.charset.Charset;
+import java.util.HashMap;
+
+public class LangUtils {
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private static HashMap<String, String> languages;
+ static {
+ languages = new HashMap<String, String>();
+ languages.put("af", "Afrikaans");
+ languages.put("sq", "Albanian");
+ languages.put("sq", "Albanian");
+ languages.put("eu", "Basque");
+ languages.put("be", "Belarusian");
+ languages.put("bg", "Bulgarian");
+ languages.put("ca", "Catalan");
+ languages.put("Chinese (Simplified)", "zh-cn");
+ languages.put("Chinese (Traditional)", "zh-tw");
+ languages.put("hr", "Croatian");
+ languages.put("cs", "Czech");
+ languages.put("da", "Danish");
+ languages.put("nl", "Dutch");
+ languages.put("nl-be", "Dutch (Belgium)");
+ languages.put("nl-nl", "Dutch (Netherlands)");
+ languages.put("en", "English");
+ languages.put("en-au", "English (Australia)");
+ languages.put("en-bz", "English (Belize)");
+ languages.put("en-ca", "English (Canada)");
+ languages.put("en-ie", "English (Ireland)");
+ languages.put("en-jm", "English (Jamaica)");
+ languages.put("en-nz", "English (New Zealand)");
+ languages.put("en-ph", "English (Phillipines)");
+ languages.put("en-za", "English (South Africa)");
+ languages.put("en-tt", "English (Trinidad)");
+ languages.put("en-gb", "English (United Kingdom)");
+ languages.put("en-us", "English (United States)");
+ languages.put("en-zw", "English (Zimbabwe)");
+ languages.put("et", "Estonian");
+ languages.put("fo", "Faeroese");
+ languages.put("fi", "Finnish");
+ languages.put("fr", "French");
+ languages.put("fr-be", "French (Belgium)");
+ languages.put("fr-ca", "French (Canada)");
+ languages.put("fr-fr", "French (France)");
+ languages.put("fr-lu", "French (Luxembourg)");
+ languages.put("fr-mc", "French (Monaco)");
+ languages.put("fr-ch", "French (Switzerland)");
+ languages.put("gl", "Galician");
+ languages.put("gd", "Gaelic");
+ languages.put("de", "German");
+ languages.put("de-at", "German (Austria)");
+ languages.put("de-de", "German (Germany)");
+ languages.put("de-li", "German (Liechtenstein)");
+ languages.put("de-lu", "German (Luxembourg)");
+ languages.put("de-ch", "German (Switzerland)");
+ languages.put("el", "Greek");
+ languages.put("haw", "Hawaiian");
+ languages.put("hu", "Hungarian");
+ languages.put("is", "Icelandic");
+ languages.put("in", "Indonesian");
+ languages.put("ga", "Irish");
+ languages.put("it", "Italian");
+ languages.put("it-it", "Italian (Italy)");
+ languages.put("it-ch", "Italian (Switzerland)");
+ languages.put("ja", "Japanese");
+ languages.put("ko", "Korean");
+ languages.put("mk", "Macedonian");
+ languages.put("no", "Norwegian");
+ languages.put("pl", "Polish");
+ languages.put("pt", "Portugese");
+ languages.put("pt-br", "Portugese (Brazil)");
+ languages.put("pt-pt", "Portugese (Portugal");
+ languages.put("ro", "Romanian");
+ languages.put("ro-mo", "Romanian (Moldova)");
+ languages.put("ro-ro", "Romanian (Romania");
+ languages.put("ru", "Russian");
+ languages.put("ru-mo", "Russian (Moldova)");
+ languages.put("ru-ru", "Russian (Russia)");
+ languages.put("sr", "Serbian");
+ languages.put("sk", "Slovak");
+ languages.put("sl", "Slovenian");
+ languages.put("es", "Spanish");
+ languages.put("es-ar", "Spanish (Argentinia)");
+ languages.put("es=bo", "Spanish (Bolivia)");
+ languages.put("es-cl", "Spanish (Chile)");
+ languages.put("es-co", "Spanish (Colombia)");
+ languages.put("es-cr", "Spanish (Costa Rica)");
+ languages.put("es-do", "Spanish (Dominican Republic)");
+ languages.put("es-ec", "Spanish (Ecuador)");
+ languages.put("es-sv", "Spanish (El Salvador)");
+ languages.put("es-gt", "Spanish (Guatemala)");
+ languages.put("es-hn", "Spanish (Honduras)");
+ languages.put("es-mx", "Spanish (Mexico)");
+ languages.put("es-ni", "Spanish (Nicaragua)");
+ languages.put("es-pa", "Spanish (Panama)");
+ languages.put("es-py", "Spanish (Paraguay)");
+ languages.put("es-pe", "Spanish (Peru)");
+ languages.put("es-pr", "Spanish (Puerto Rico)");
+ languages.put("es-es", "Spanish (Spain)");
+ languages.put("es-uy", "Spanish (Uruguay)");
+ languages.put("es-ve", "Spanish (Venezuela)");
+ languages.put("sv", "Swedish");
+ languages.put("sv-fi", "Swedish (Finland)");
+ languages.put("sv-se", "Swedish (Sweden)");
+ languages.put("tr", "Turkish");
+ languages.put("uk", "Ukranian");
+ }
+
+ /** Finds language string for key or returns the language key if it can't be found. */
+ public static String getLanguageString(String key) {
+ String language = languages.get(key);
+ if (language != null) {
+ return language;
+ } else {
+ return key;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
new file mode 100644
index 000000000..b321536a3
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.core.util;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class NetworkUtils {
+ private static final String TAG = "NetworkUtils";
+
+ private NetworkUtils() {
+
+ }
+
+ /**
+ * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for
+ * automatic downloads is disabled or the device is connected to a Wi-Fi
+ * network that is on the 'selected networks' list of the Wi-Fi filter for
+ * automatic downloads and false otherwise.
+ * */
+ public static boolean autodownloadNetworkAvailable(Context context) {
+ ConnectivityManager cm = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ if (networkInfo != null) {
+ if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Device is connected to Wi-Fi");
+ if (networkInfo.isConnected()) {
+ if (!UserPreferences.isEnableAutodownloadWifiFilter()) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Auto-dl filter is disabled");
+ return true;
+ } else {
+ WifiManager wm = (WifiManager) context
+ .getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = wm.getConnectionInfo();
+ List<String> selectedNetworks = Arrays
+ .asList(UserPreferences
+ .getAutodownloadSelectedNetworks());
+ if (selectedNetworks.contains(Integer.toString(wifiInfo
+ .getNetworkId()))) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "Current network is on the selected networks list");
+ return true;
+ }
+ }
+ }
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Network for auto-dl is not available");
+ return false;
+ }
+
+ public static boolean networkAvailable(Context context) {
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ return info != null && info.isConnected();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java
new file mode 100644
index 000000000..8e40ae184
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java
@@ -0,0 +1,93 @@
+package de.danoeh.antennapod.core.util;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that
+ * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List.
+ * QueueAccess provides an interface for accessing the queue without having to care about the type of the queue
+ * representation.
+ */
+public abstract class QueueAccess {
+ /**
+ * Returns true if the item is in the queue, false otherwise.
+ */
+ public abstract boolean contains(long id);
+
+ /**
+ * Removes the item from the queue.
+ *
+ * @return true if the queue was modified by this operation.
+ */
+ public abstract boolean remove(long id);
+
+ private QueueAccess() {
+
+ }
+
+ public static QueueAccess IDListAccess(final List<Long> ids) {
+ return new QueueAccess() {
+ @Override
+ public boolean contains(long id) {
+ return (ids != null) && ids.contains(id);
+ }
+
+ @Override
+ public boolean remove(long id) {
+ return ids.remove(id);
+ }
+
+
+ };
+ }
+
+ public static QueueAccess ItemListAccess(final List<FeedItem> items) {
+ return new QueueAccess() {
+ @Override
+ public boolean contains(long id) {
+ if (items == null) {
+ return false;
+ }
+ for (FeedItem item : items) {
+ if (item.getId() == id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean remove(long id) {
+ Iterator<FeedItem> it = items.iterator();
+ FeedItem item;
+ while (it.hasNext()) {
+ item = it.next();
+ if (item.getId() == id) {
+ it.remove();
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ public static QueueAccess NotInQueueAccess() {
+ return new QueueAccess() {
+ @Override
+ public boolean contains(long id) {
+ return false;
+ }
+
+ @Override
+ public boolean remove(long id) {
+ return false;
+ }
+ };
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java
new file mode 100644
index 000000000..85f32ed50
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.core.util;
+
+import android.content.Context;
+import android.content.Intent;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+/** Utility methods for sharing data */
+public class ShareUtils {
+ private static final String TAG = "ShareUtils";
+
+ private ShareUtils() {}
+
+ public static void shareLink(Context context, String link) {
+ Intent i = new Intent(Intent.ACTION_SEND);
+ i.setType("text/plain");
+ i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL");
+ i.putExtra(Intent.EXTRA_TEXT, link);
+ context.startActivity(Intent.createChooser(i, "Share URL"));
+ }
+
+ public static void shareFeedItemLink(Context context, FeedItem item) {
+ shareLink(context, item.getLink());
+ }
+
+ public static void shareFeedDownloadLink(Context context, Feed feed) {
+ shareLink(context, feed.getDownload_url());
+ }
+
+ public static void shareFeedlink(Context context, Feed feed) {
+ shareLink(context, feed.getLink());
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java
new file mode 100644
index 000000000..7e7c6c08b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java
@@ -0,0 +1,16 @@
+package de.danoeh.antennapod.core.util;
+
+import java.util.concurrent.Callable;
+
+/**
+ * Created by daniel on 04.08.13.
+ */
+public interface ShownotesProvider {
+ /**
+ * Loads shownotes. If the shownotes have to be loaded from a file or from a
+ * database, it should be done in a separate thread. After the shownotes
+ * have been loaded, callback.onShownotesLoaded should be called.
+ */
+ public Callable<String> loadShownotes();
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
new file mode 100644
index 000000000..dea380937
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.core.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.os.StatFs;
+import android.util.Log;
+
+import java.io.File;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+/**
+ * Utility functions for handling storage errors
+ */
+public class StorageUtils {
+ private static final String TAG = "StorageUtils";
+
+ public static boolean storageAvailable(Context context) {
+ File dir = UserPreferences.getDataFolder(context, null);
+ if (dir != null) {
+ return dir.exists() && dir.canRead() && dir.canWrite();
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Storage not available: data folder is null");
+ return false;
+ }
+ }
+
+ /**
+ * Checks if external storage is available. If external storage isn't
+ * available, the current activity is finsished an an error activity is
+ * launched.
+ *
+ * @param activity the activity which would be finished if no storage is
+ * available
+ * @return true if external storage is available
+ */
+ public static boolean checkStorageAvailability(Activity activity) {
+ boolean storageAvailable = storageAvailable(activity);
+ if (!storageAvailable) {
+ activity.finish();
+ activity.startActivity(ClientConfig.applicationCallbacks.getStorageErrorActivity(activity));
+ }
+ return storageAvailable;
+ }
+
+ /**
+ * Get the number of free bytes that are available on the external storage.
+ */
+ public static long getFreeSpaceAvailable() {
+ StatFs stat = new StatFs(UserPreferences.getDataFolder(
+ ClientConfig.applicationCallbacks.getApplicationInstance(), null).getAbsolutePath());
+ long availableBlocks;
+ long blockSize;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ availableBlocks = stat.getAvailableBlocksLong();
+ blockSize = stat.getBlockSizeLong();
+ } else {
+ availableBlocks = stat.getAvailableBlocks();
+ blockSize = stat.getBlockSize();
+ }
+ return availableBlocks * blockSize;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java
new file mode 100644
index 000000000..f67367643
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java
@@ -0,0 +1,23 @@
+package de.danoeh.antennapod.core.util;
+
+import android.util.Log;
+
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+public class ThemeUtils {
+ private static final String TAG = "ThemeUtils";
+
+ public static int getSelectionBackgroundColor() {
+ int theme = UserPreferences.getTheme();
+ if (theme == R.style.Theme_AntennaPod_Dark) {
+ return R.color.selection_background_color_dark;
+ } else if (theme == R.style.Theme_AntennaPod_Light) {
+ return R.color.selection_background_color_light;
+ } else {
+ Log.e(TAG,
+ "getSelectionBackgroundColor could not match the current theme to any color!");
+ return R.color.selection_background_color_light;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java
new file mode 100644
index 000000000..092c06b4a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.core.util;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/**
+ * Utility methods for dealing with URL encoding.
+ */
+public class URIUtil {
+ private static final String TAG = "URIUtil";
+
+ private URIUtil() {}
+
+ public static URI getURIFromRequestUrl(String source) {
+ // try without encoding the URI
+ try {
+ return new URI(source);
+ } catch (URISyntaxException e) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Source is not encoded, encoding now");
+ }
+ try {
+ URL url = new URL(source);
+ return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java
new file mode 100644
index 000000000..4bd18c8bc
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java
@@ -0,0 +1,54 @@
+package de.danoeh.antennapod.core.util;
+
+import android.util.Log;
+
+import org.apache.commons.lang3.StringUtils;
+
+import de.danoeh.antennapod.core.BuildConfig;
+
+/**
+ * Provides methods for checking and editing a URL.
+ */
+public final class URLChecker {
+
+ /**
+ * Class shall not be instantiated.
+ */
+ private URLChecker() {
+ }
+
+ /**
+ * Logging tag.
+ */
+ private static final String TAG = "URLChecker";
+
+ private static final String AP_SUBSCRIBE = "antennapod-subscribe://";
+
+ /**
+ * Checks if URL is valid and modifies it if necessary.
+ *
+ * @param url The url which is going to be prepared
+ * @return The prepared url
+ */
+ public static String prepareURL(String url) {
+ url = StringUtils.trim(url);
+ if (url.startsWith("feed://")) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://");
+ return url.replaceFirst("feed://", "http://");
+ } else if (url.startsWith("pcast://")) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Removing pcast://");
+ return prepareURL(StringUtils.removeStart(url, "pcast://"));
+ } else if (url.startsWith("itpc")) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://");
+ return url.replaceFirst("itpc://", "http://");
+ } else if (url.startsWith(AP_SUBSCRIBE)) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Removing antennapod-subscribe://");
+ return prepareURL(StringUtils.removeStart(url, AP_SUBSCRIBE));
+ } else if (!(url.startsWith("http://") || url.startsWith("https://"))) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL");
+ return "http://" + url;
+ } else {
+ return url;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java
new file mode 100644
index 000000000..5843c5f8f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2012 Roman Nurik
+ *
+ * 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.
+ */
+
+package de.danoeh.antennapod.core.util;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+import com.nineoldandroids.animation.Animator;
+import com.nineoldandroids.animation.AnimatorListenerAdapter;
+import com.nineoldandroids.view.ViewHelper;
+import com.nineoldandroids.view.ViewPropertyAnimator;
+import de.danoeh.antennapod.core.R;
+
+import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
+
+public class UndoBarController {
+ private View mBarView;
+ private TextView mMessageView;
+ private ViewPropertyAnimator mBarAnimator;
+ private Handler mHideHandler = new Handler();
+
+ private UndoListener mUndoListener;
+
+ // State objects
+ private Parcelable mUndoToken;
+ private CharSequence mUndoMessage;
+
+ public interface UndoListener {
+ void onUndo(Parcelable token);
+ }
+
+ public UndoBarController(View undoBarView, UndoListener undoListener) {
+ mBarView = undoBarView;
+ mBarAnimator = animate(mBarView);
+ mUndoListener = undoListener;
+
+ mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message);
+ mBarView.findViewById(R.id.undobar_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ hideUndoBar(false);
+ mUndoListener.onUndo(mUndoToken);
+ }
+ });
+
+ hideUndoBar(true);
+ }
+
+ public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) {
+ mUndoToken = undoToken;
+ mUndoMessage = message;
+ mMessageView.setText(mUndoMessage);
+
+ mHideHandler.removeCallbacks(mHideRunnable);
+ mHideHandler.postDelayed(mHideRunnable,
+ mBarView.getResources().getInteger(R.integer.undobar_hide_delay));
+
+ mBarView.setVisibility(View.VISIBLE);
+ if (immediate) {
+ ViewHelper.setAlpha(mBarView, 1);
+ } else {
+ mBarAnimator.cancel();
+ mBarAnimator
+ .alpha(1)
+ .setDuration(
+ mBarView.getResources()
+ .getInteger(android.R.integer.config_shortAnimTime))
+ .setListener(null);
+ }
+ }
+
+ public void hideUndoBar(boolean immediate) {
+ mHideHandler.removeCallbacks(mHideRunnable);
+ if (immediate) {
+ mBarView.setVisibility(View.GONE);
+ ViewHelper.setAlpha(mBarView, 0);
+ mUndoMessage = null;
+ mUndoToken = null;
+
+ } else {
+ mBarAnimator.cancel();
+ mBarAnimator
+ .alpha(0)
+ .setDuration(mBarView.getResources()
+ .getInteger(android.R.integer.config_shortAnimTime))
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBarView.setVisibility(View.GONE);
+ mUndoMessage = null;
+ mUndoToken = null;
+ }
+ });
+ }
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putCharSequence("undo_message", mUndoMessage);
+ outState.putParcelable("undo_token", mUndoToken);
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mUndoMessage = savedInstanceState.getCharSequence("undo_message");
+ mUndoToken = savedInstanceState.getParcelable("undo_token");
+
+ if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) {
+ showUndoBar(true, mUndoMessage, mUndoToken);
+ }
+ }
+ }
+
+ private Runnable mHideRunnable = new Runnable() {
+ @Override
+ public void run() {
+ hideUndoBar(false);
+ }
+ };
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java
new file mode 100644
index 000000000..5274ffc9e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.core.util.comparator;
+
+import de.danoeh.antennapod.core.feed.Chapter;
+
+import java.util.Comparator;
+
+public class ChapterStartTimeComparator implements Comparator<Chapter> {
+
+ @Override
+ public int compare(Chapter lhs, Chapter rhs) {
+ if (lhs.getStart() == rhs.getStart()) {
+ return 0;
+ } else if (lhs.getStart() < rhs.getStart()) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java
new file mode 100644
index 000000000..ebdbfe2a5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.core.util.comparator;
+
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+
+import java.util.Comparator;
+
+/** Compares the completion date of two Downloadstatus objects. */
+public class DownloadStatusComparator implements Comparator<DownloadStatus> {
+
+ @Override
+ public int compare(DownloadStatus lhs, DownloadStatus rhs) {
+ return rhs.getCompletionDate().compareTo(lhs.getCompletionDate());
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java
new file mode 100644
index 000000000..a1f3ec699
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java
@@ -0,0 +1,19 @@
+package de.danoeh.antennapod.core.util.comparator;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+import java.util.Comparator;
+
+/** Compares the pubDate of two FeedItems for sorting*/
+public class FeedItemPubdateComparator implements Comparator<FeedItem> {
+
+ /** Returns a new instance of this comparator in reverse order.
+ public static FeedItemPubdateComparator newInstance() {
+ FeedItemPubdateComparator
+ }*/
+ @Override
+ public int compare(FeedItem lhs, FeedItem rhs) {
+ return rhs.getPubDate().compareTo(lhs.getPubDate());
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java
new file mode 100644
index 000000000..84d244660
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java
@@ -0,0 +1,19 @@
+package de.danoeh.antennapod.core.util.comparator;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+import java.util.Comparator;
+
+public class PlaybackCompletionDateComparator implements Comparator<FeedItem> {
+
+ public int compare(FeedItem lhs, FeedItem rhs) {
+ if (lhs.getMedia() != null
+ && lhs.getMedia().getPlaybackCompletionDate() != null
+ && rhs.getMedia() != null
+ && rhs.getMedia().getPlaybackCompletionDate() != null) {
+ return rhs.getMedia().getPlaybackCompletionDate()
+ .compareTo(lhs.getMedia().getPlaybackCompletionDate());
+ }
+ return 0;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java
new file mode 100644
index 000000000..b16e0949d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java
@@ -0,0 +1,14 @@
+package de.danoeh.antennapod.core.util.comparator;
+
+import de.danoeh.antennapod.core.feed.SearchResult;
+
+import java.util.Comparator;
+
+public class SearchResultValueComparator implements Comparator<SearchResult> {
+
+ @Override
+ public int compare(SearchResult lhs, SearchResult rhs) {
+ return rhs.getValue() - lhs.getValue();
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java
new file mode 100644
index 000000000..287fe1100
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java
@@ -0,0 +1,23 @@
+package de.danoeh.antennapod.core.util.exception;
+
+import de.danoeh.antennapod.core.feed.FeedMedia;
+
+public class MediaFileNotFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private FeedMedia media;
+
+ public MediaFileNotFoundException(String msg, FeedMedia media) {
+ super(msg);
+ this.media = media;
+ }
+
+ public MediaFileNotFoundException(FeedMedia media) {
+ super();
+ this.media = media;
+ }
+
+ public FeedMedia getMedia() {
+ return media;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java
new file mode 100644
index 000000000..e4818214e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.core.util.flattr;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import org.shredzone.flattr4j.FlattrFactory;
+import org.shredzone.flattr4j.FlattrService;
+import org.shredzone.flattr4j.oauth.AccessToken;
+
+/** Ensures that only one instance of the FlattrService class exists at a time */
+
+public class FlattrServiceCreator {
+ public static final String TAG = "FlattrServiceCreator";
+
+ private static volatile FlattrService flattrService;
+
+ public static FlattrService getService(AccessToken token) {
+ return FlattrFactory.getInstance().createFlattrService(token);
+ }
+
+ public static void deleteFlattrService() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Deleting service instance");
+ flattrService = null;
+ }
+}
+
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java
new file mode 100644
index 000000000..d82171d1a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java
@@ -0,0 +1,68 @@
+package de.danoeh.antennapod.core.util.flattr;
+
+import java.util.Calendar;
+
+public class FlattrStatus {
+ public static final int STATUS_UNFLATTERED = 0;
+ public static final int STATUS_QUEUE = 1;
+ public static final int STATUS_FLATTRED = 2;
+
+ private int status = STATUS_UNFLATTERED;
+ private Calendar lastFlattred;
+
+ public FlattrStatus() {
+ status = STATUS_UNFLATTERED;
+ lastFlattred = Calendar.getInstance();
+ }
+
+ public FlattrStatus(long status) {
+ lastFlattred = Calendar.getInstance();
+ fromLong(status);
+ }
+
+ public void setFlattred() {
+ status = STATUS_FLATTRED;
+ lastFlattred = Calendar.getInstance();
+ }
+
+ public void setUnflattred() {
+ status = STATUS_UNFLATTERED;
+ }
+
+ public boolean getUnflattred() {
+ return status == STATUS_UNFLATTERED;
+ }
+
+ public void setFlattrQueue() {
+ if (flattrable())
+ status = STATUS_QUEUE;
+ }
+
+ public void fromLong(long status) {
+ if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE)
+ this.status = (int) status;
+ else {
+ this.status = STATUS_FLATTRED;
+ lastFlattred.setTimeInMillis(status);
+ }
+ }
+
+ public long toLong() {
+ if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE)
+ return status;
+ else {
+ return lastFlattred.getTimeInMillis();
+ }
+ }
+
+ public boolean flattrable() {
+ Calendar firstOfMonth = Calendar.getInstance();
+ firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH));
+
+ return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) );
+ }
+
+ public boolean getFlattrQueue() {
+ return status == STATUS_QUEUE;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java
new file mode 100644
index 000000000..515028ab6
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java
@@ -0,0 +1,7 @@
+package de.danoeh.antennapod.core.util.flattr;
+
+public interface FlattrThing {
+ public String getTitle();
+ public String getPaymentLink();
+ public FlattrStatus getFlattrStatus();
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java
new file mode 100644
index 000000000..90caad946
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java
@@ -0,0 +1,304 @@
+package de.danoeh.antennapod.core.util.flattr;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.apache.commons.lang3.StringUtils;
+import org.shredzone.flattr4j.FlattrService;
+import org.shredzone.flattr4j.exception.FlattrException;
+import org.shredzone.flattr4j.model.Flattr;
+import org.shredzone.flattr4j.model.Thing;
+import org.shredzone.flattr4j.oauth.AccessToken;
+import org.shredzone.flattr4j.oauth.AndroidAuthenticator;
+import org.shredzone.flattr4j.oauth.Scope;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.TimeZone;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.asynctask.FlattrTokenFetcher;
+import de.danoeh.antennapod.core.storage.DBWriter;
+
+/**
+ * Utility methods for doing something with flattr.
+ */
+
+public class FlattrUtils {
+ private static final String TAG = "FlattrUtils";
+
+ private static final String HOST_NAME = "de.danoeh.antennapod";
+
+ private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken";
+
+ // Flattr URL for this app.
+ public static final String APP_URL = "http://antennapod.com";
+ // Human-readable flattr-page.
+ public static final String APP_LINK = "https://flattr.com/thing/745609/";
+ public static final String APP_THING_ID = "745609";
+
+ private static volatile AccessToken cachedToken;
+
+ private static AndroidAuthenticator createAuthenticator() {
+ return new AndroidAuthenticator(HOST_NAME, ClientConfig.flattrCallbacks.getFlattrAppKey(),
+ ClientConfig.flattrCallbacks.getFlattrAppSecret());
+ }
+
+ public static void startAuthProcess(Context context) throws FlattrException {
+ AndroidAuthenticator auth = createAuthenticator();
+ auth.setScope(EnumSet.of(Scope.FLATTR));
+ Intent intent = auth.createAuthenticateIntent();
+ context.startActivity(intent);
+ }
+
+ private static AccessToken retrieveToken() {
+ if (cachedToken == null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Retrieving access token");
+ String token = PreferenceManager.getDefaultSharedPreferences(
+ ClientConfig.applicationCallbacks.getApplicationInstance())
+ .getString(PREF_ACCESS_TOKEN, null);
+ if (token != null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Found access token. Caching.");
+ cachedToken = new AccessToken(token);
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "No access token found");
+ return null;
+ }
+ }
+ return cachedToken;
+
+ }
+
+ /**
+ * Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty
+ */
+ public static boolean hasAPICredentials() {
+ return StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppKey())
+ && StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppSecret());
+ }
+
+ public static boolean hasToken() {
+ return retrieveToken() != null;
+ }
+
+ public static void storeToken(AccessToken token) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Storing token");
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(ClientConfig.applicationCallbacks.getApplicationInstance()).edit();
+ if (token != null) {
+ editor.putString(PREF_ACCESS_TOKEN, token.getToken());
+ } else {
+ editor.putString(PREF_ACCESS_TOKEN, null);
+ }
+ editor.commit();
+ cachedToken = token;
+ }
+
+ public static void deleteToken() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Deleting flattr token");
+ storeToken(null);
+ }
+
+ public static Thing getAppThing(Context context) {
+ FlattrService fs = FlattrServiceCreator.getService(retrieveToken());
+ try {
+ Thing thing = fs.getThing(Thing.withId(APP_THING_ID));
+ return thing;
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ showErrorDialog(context, e.getMessage());
+ return null;
+ }
+ }
+
+ public static void clickUrl(Context context, String url)
+ throws FlattrException {
+ if (hasToken()) {
+ FlattrService fs = FlattrServiceCreator.getService(retrieveToken());
+ fs.flattr(url);
+ } else {
+ Log.e(TAG, "clickUrl was called with null access token");
+ }
+ }
+
+ public static List<Flattr> retrieveFlattredThings()
+ throws FlattrException {
+ ArrayList<Flattr> myFlattrs = new ArrayList<Flattr>();
+
+ if (hasToken()) {
+ FlattrService fs = FlattrServiceCreator.getService(retrieveToken());
+
+ Calendar firstOfMonth = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ firstOfMonth.set(Calendar.MILLISECOND, 0);
+ firstOfMonth.set(Calendar.SECOND, 0);
+ firstOfMonth.set(Calendar.MINUTE, 0);
+ firstOfMonth.set(Calendar.HOUR_OF_DAY, 0);
+ firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH));
+
+ Date firstOfMonthDate = firstOfMonth.getTime();
+
+ // subscriptions some times get flattrd slightly before midnight - give it an hour leeway
+ firstOfMonthDate = new Date(firstOfMonthDate.getTime() - 60 * 60 * 1000);
+
+ final int FLATTR_COUNT = 30;
+ final int FLATTR_MAXPAGE = 5;
+
+ for (int page = 0; page < FLATTR_MAXPAGE; page++) {
+ for (Flattr fl : fs.getMyFlattrs(FLATTR_COUNT, page)) {
+ if (fl.getCreated().after(firstOfMonthDate))
+ myFlattrs.add(fl);
+ else
+ break;
+ }
+ }
+
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate);
+
+ for (Flattr fl : myFlattrs) {
+ Thing thing = fl.getThing();
+ Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated());
+ }
+ }
+
+ } else {
+ Log.e(TAG, "retrieveFlattrdThings was called with null access token");
+ }
+
+ return myFlattrs;
+ }
+
+ public static void handleCallback(Context context, Uri uri) {
+ AndroidAuthenticator auth = createAuthenticator();
+ new FlattrTokenFetcher(context, auth, uri).executeAsync();
+ }
+
+ public static void revokeAccessToken(Context context) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Revoking access token");
+ deleteToken();
+ FlattrServiceCreator.deleteFlattrService();
+ showRevokeDialog(context);
+ DBWriter.clearAllFlattrStatus(context);
+ }
+
+ // ------------------------------------------------ DIALOGS
+
+ public static void showRevokeDialog(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.access_revoked_title);
+ builder.setMessage(R.string.access_revoked_info);
+ builder.setNeutralButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+ builder.create().show();
+ }
+
+ /**
+ * Opens a dialog that ask the user to either connect the app with flattr or to be redirected to
+ * the thing's website.
+ * If no API credentials are available, the user will immediately be redirected to the thing's website.
+ */
+ public static void showNoTokenDialogOrRedirect(final Context context, final String url) {
+ if (hasAPICredentials()) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.no_flattr_token_title);
+ builder.setMessage(R.string.no_flattr_token_msg);
+ builder.setPositiveButton(R.string.authenticate_now_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ context.startActivity(
+ ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context));
+ }
+
+ }
+ );
+
+ builder.setNegativeButton(R.string.visit_website_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Uri uri = Uri.parse(url);
+ context.startActivity(new Intent(Intent.ACTION_VIEW,
+ uri));
+ }
+
+ }
+ );
+ builder.create().show();
+ } else {
+ Uri uri = Uri.parse(url);
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+ }
+
+ public static void showForbiddenDialog(final Context context,
+ final String url) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.action_forbidden_title);
+ builder.setMessage(R.string.action_forbidden_msg);
+ builder.setPositiveButton(R.string.authenticate_now_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ context.startActivity(
+ ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context));
+ }
+
+ }
+ );
+ builder.setNegativeButton(R.string.visit_website_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Uri uri = Uri.parse(url);
+ context.startActivity(new Intent(Intent.ACTION_VIEW,
+ uri));
+ }
+
+ }
+ );
+ builder.create().show();
+ }
+
+ public static void showErrorDialog(final Context context, final String msg) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.error_label);
+ builder.setMessage(msg);
+ builder.setNeutralButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+ builder.create().show();
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java
new file mode 100644
index 000000000..2c178496e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java
@@ -0,0 +1,30 @@
+package de.danoeh.antennapod.core.util.flattr;
+
+/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */
+public class SimpleFlattrThing implements FlattrThing {
+ public SimpleFlattrThing(String title, String url, FlattrStatus status)
+ {
+ this.title = title;
+ this.url = url;
+ this.status = status;
+ }
+
+ public String getTitle()
+ {
+ return this.title;
+ }
+
+ public String getPaymentLink()
+ {
+ return this.url;
+ }
+
+ public FlattrStatus getFlattrStatus()
+ {
+ return this.status;
+ }
+
+ private String title;
+ private String url;
+ private FlattrStatus status;
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java
new file mode 100644
index 000000000..17581d3e9
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.core.util.gui;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import de.danoeh.antennapod.core.feed.FeedItem;
+
+/**
+ * Used by an UndoBarController for saving a removed FeedItem
+ */
+public class FeedItemUndoToken implements Parcelable {
+ private long itemId;
+ private long feedId;
+ private int position;
+
+ public FeedItemUndoToken(FeedItem item, int position) {
+ this.itemId = item.getId();
+ this.feedId = item.getFeed().getId();
+ this.position = position;
+ }
+
+ private FeedItemUndoToken(Parcel in) {
+ itemId = in.readLong();
+ feedId = in.readLong();
+ position = in.readInt();
+ }
+
+ public static final Parcelable.Creator<FeedItemUndoToken> CREATOR = new Parcelable.Creator<FeedItemUndoToken>() {
+ public FeedItemUndoToken createFromParcel(Parcel in) {
+ return new FeedItemUndoToken(in);
+ }
+
+ public FeedItemUndoToken[] newArray(int size) {
+ return new FeedItemUndoToken[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(itemId);
+ out.writeLong(feedId);
+ out.writeInt(position);
+ }
+
+ public long getFeedItemId() {
+ return itemId;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+}
+
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java
new file mode 100644
index 000000000..9f3c4c6d5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java
@@ -0,0 +1,118 @@
+package de.danoeh.antennapod.core.util.id3reader;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.ID3Chapter;
+import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
+import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ChapterReader extends ID3Reader {
+ private static final String TAG = "ID3ChapterReader";
+
+ private static final String FRAME_ID_CHAPTER = "CHAP";
+ private static final String FRAME_ID_TITLE = "TIT2";
+ private static final String FRAME_ID_LINK = "WXXX";
+
+ private List<Chapter> chapters;
+ private ID3Chapter currentChapter;
+
+ @Override
+ public int onStartTagHeader(TagHeader header) {
+ chapters = new ArrayList<Chapter>();
+ System.out.println(header.toString());
+ return ID3Reader.ACTION_DONT_SKIP;
+ }
+
+ @Override
+ public int onStartFrameHeader(FrameHeader header, InputStream input)
+ throws IOException, ID3ReaderException {
+ System.out.println(header.toString());
+ if (header.getId().equals(FRAME_ID_CHAPTER)) {
+ if (currentChapter != null) {
+ if (!hasId3Chapter(currentChapter)) {
+ chapters.add(currentChapter);
+ if (BuildConfig.DEBUG) Log.d(TAG, "Found chapter: " + currentChapter);
+ currentChapter = null;
+ }
+ }
+ StringBuffer elementId = new StringBuffer();
+ readISOString(elementId, input, Integer.MAX_VALUE);
+ char[] startTimeSource = readBytes(input, 4);
+ long startTime = ((int) startTimeSource[0] << 24)
+ | ((int) startTimeSource[1] << 16)
+ | ((int) startTimeSource[2] << 8) | startTimeSource[3];
+ currentChapter = new ID3Chapter(elementId.toString(), startTime);
+ skipBytes(input, 12);
+ return ID3Reader.ACTION_DONT_SKIP;
+ } else if (header.getId().equals(FRAME_ID_TITLE)) {
+ if (currentChapter != null && currentChapter.getTitle() == null) {
+ StringBuffer title = new StringBuffer();
+ readString(title, input, header.getSize());
+ currentChapter
+ .setTitle(title.toString());
+ if (BuildConfig.DEBUG) Log.d(TAG, "Found title: " + currentChapter.getTitle());
+
+ return ID3Reader.ACTION_DONT_SKIP;
+ }
+ } else if (header.getId().equals(FRAME_ID_LINK)) {
+ if (currentChapter != null) {
+ // skip description
+ int descriptionLength = readString(null, input, header.getSize());
+ StringBuffer link = new StringBuffer();
+ readISOString(link, input, header.getSize() - descriptionLength);
+ String decodedLink = URLDecoder.decode(link.toString(), "UTF-8");
+
+ currentChapter.setLink(decodedLink);
+
+ if (BuildConfig.DEBUG) Log.d(TAG, "Found link: " + currentChapter.getLink());
+ return ID3Reader.ACTION_DONT_SKIP;
+ }
+ } else if (header.getId().equals("APIC")) {
+ Log.d(TAG, header.toString());
+ }
+
+ return super.onStartFrameHeader(header, input);
+ }
+
+ private boolean hasId3Chapter(ID3Chapter chapter) {
+ for (Chapter c : chapters) {
+ if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onEndTag() {
+ if (currentChapter != null) {
+ if (!hasId3Chapter(currentChapter)) {
+ chapters.add(currentChapter);
+ }
+ }
+ System.out.println("Reached end of tag");
+ if (chapters != null) {
+ for (Chapter c : chapters) {
+ System.out.println(c.toString());
+ }
+ }
+ }
+
+ @Override
+ public void onNoTagHeaderFound() {
+ System.out.println("No tag header found");
+ super.onNoTagHeaderFound();
+ }
+
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java
new file mode 100644
index 000000000..a238c11e9
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java
@@ -0,0 +1,250 @@
+package de.danoeh.antennapod.core.util.id3reader;
+
+import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
+import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Reads the ID3 Tag of a given file. In order to use this class, you should
+ * create a subclass of it and overwrite the onStart* - or onEnd* - methods.
+ */
+public class ID3Reader {
+ private static final int HEADER_LENGTH = 10;
+ private static final int ID3_LENGTH = 3;
+ private static final int FRAME_ID_LENGTH = 4;
+
+ protected static final int ACTION_SKIP = 1;
+ protected static final int ACTION_DONT_SKIP = 2;
+
+ protected int readerPosition;
+
+ private static final byte ENCODING_UTF16_WITH_BOM = 1;
+ private static final byte ENCODING_UTF16_WITHOUT_BOM = 2;
+ private static final byte ENCODING_UTF8 = 3;
+
+ private TagHeader tagHeader;
+
+ public ID3Reader() {
+ }
+
+ public final void readInputStream(InputStream input) throws IOException,
+ ID3ReaderException {
+ int rc;
+ readerPosition = 0;
+ char[] tagHeaderSource = readBytes(input, HEADER_LENGTH);
+ tagHeader = createTagHeader(tagHeaderSource);
+ if (tagHeader == null) {
+ onNoTagHeaderFound();
+ } else {
+ rc = onStartTagHeader(tagHeader);
+ if (rc == ACTION_SKIP) {
+ onEndTag();
+ } else {
+ while (readerPosition < tagHeader.getSize()) {
+ FrameHeader frameHeader = createFrameHeader(readBytes(
+ input, HEADER_LENGTH));
+ if (checkForNullString(frameHeader.getId())) {
+ break;
+ } else {
+ rc = onStartFrameHeader(frameHeader, input);
+ if (rc == ACTION_SKIP) {
+
+ if (frameHeader.getSize() + readerPosition > tagHeader
+ .getSize()) {
+ break;
+ } else {
+ skipBytes(input, frameHeader.getSize());
+ }
+ }
+ }
+ }
+ onEndTag();
+ }
+ }
+ }
+
+ /** Returns true if string only contains null-bytes. */
+ private boolean checkForNullString(String s) {
+ if (!s.isEmpty()) {
+ int i = 0;
+ if (s.charAt(i) == 0) {
+ for (i = 1; i < s.length(); i++) {
+ if (s.charAt(i) != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ } else {
+ return true;
+ }
+
+ }
+
+ /**
+ * Read a certain number of bytes from the given input stream. This method
+ * changes the readerPosition-attribute.
+ */
+ protected char[] readBytes(InputStream input, int number)
+ throws IOException, ID3ReaderException {
+ char[] header = new char[number];
+ for (int i = 0; i < number; i++) {
+ int b = input.read();
+ readerPosition++;
+ if (b != -1) {
+ header[i] = (char) b;
+ } else {
+ throw new ID3ReaderException("Unexpected end of stream");
+ }
+ }
+ return header;
+ }
+
+ /**
+ * Skip a certain number of bytes on the given input stream. This method
+ * changes the readerPosition-attribute.
+ */
+ protected void skipBytes(InputStream input, int number) throws IOException {
+ if (number <= 0) {
+ number = 1;
+ }
+ IOUtils.skipFully(input, number);
+
+ readerPosition += number;
+ }
+
+ private TagHeader createTagHeader(char[] source) throws ID3ReaderException {
+ boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44)
+ && (source[2] == 0x33);
+ if (source.length != HEADER_LENGTH) {
+ throw new ID3ReaderException("Length of header must be "
+ + HEADER_LENGTH);
+ }
+ if (hasTag) {
+ String id = new String(source, 0, ID3_LENGTH);
+ char version = (char) ((source[3] << 8) | source[4]);
+ byte flags = (byte) source[5];
+ int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8)
+ | source[9];
+ size = unsynchsafe(size);
+ return new TagHeader(id, size, version, flags);
+ } else {
+ return null;
+ }
+ }
+
+ private FrameHeader createFrameHeader(char[] source)
+ throws ID3ReaderException {
+ if (source.length != HEADER_LENGTH) {
+ throw new ID3ReaderException("Length of header must be "
+ + HEADER_LENGTH);
+ }
+ String id = new String(source, 0, FRAME_ID_LENGTH);
+
+ int size = (((int) source[4]) << 24) | (((int) source[5]) << 16)
+ | (((int) source[6]) << 8) | source[7];
+ if (tagHeader != null && tagHeader.getVersion() >= 0x0400) {
+ size = unsynchsafe(size);
+ }
+ char flags = (char) ((source[8] << 8) | source[9]);
+ return new FrameHeader(id, size, flags);
+ }
+
+ private int unsynchsafe(int in) {
+ int out = 0;
+ int mask = 0x7F000000;
+
+ while (mask != 0) {
+ out >>= 1;
+ out |= in & mask;
+ mask >>= 8;
+ }
+
+ return out;
+ }
+
+ protected int readString(StringBuffer buffer, InputStream input, int max) throws IOException,
+ ID3ReaderException {
+ if (max > 0) {
+ char[] encoding = readBytes(input, 1);
+ max--;
+
+ if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) {
+ return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account
+ } else if (encoding[0] == ENCODING_UTF8) {
+ return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account
+ } else {
+ return readISOString(buffer, input, max) + 1; // take encoding byte into account
+ }
+ } else {
+ if (buffer != null) {
+ buffer.append("");
+ }
+ return 0;
+ }
+ }
+
+ protected int readISOString(StringBuffer buffer, InputStream input, int max)
+ throws IOException, ID3ReaderException {
+
+ int bytesRead = 0;
+ char c;
+ while (++bytesRead <= max && (c = (char) input.read()) > 0) {
+ if (buffer != null) {
+ buffer.append(c);
+ }
+ }
+ return bytesRead;
+ }
+
+ private int readUnicodeString(StringBuffer strBuffer, InputStream input, int max, Charset charset)
+ throws IOException, ID3ReaderException {
+ byte[] buffer = new byte[max];
+ int c, cZero = -1;
+ int i = 0;
+ for (; i < max; i++) {
+ c = input.read();
+ if (c == -1) {
+ break;
+ } else if (c == 0) {
+ if (cZero == 0) {
+ // termination character found
+ break;
+ } else {
+ cZero = 0;
+ }
+ } else {
+ buffer[i] = (byte) c;
+ cZero = -1;
+ }
+ }
+ if (strBuffer != null) {
+ strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString());
+ }
+ return i;
+ }
+
+ public int onStartTagHeader(TagHeader header) {
+ return ACTION_SKIP;
+ }
+
+ public int onStartFrameHeader(FrameHeader header, InputStream input)
+ throws IOException, ID3ReaderException {
+ return ACTION_SKIP;
+ }
+
+ public void onEndTag() {
+
+ }
+
+ public void onNoTagHeaderFound() {
+
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java
new file mode 100644
index 000000000..0c746d7e5
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.core.util.id3reader;
+
+public class ID3ReaderException extends Exception {
+
+ public ID3ReaderException() {
+ }
+
+ public ID3ReaderException(String arg0) {
+ super(arg0);
+ }
+
+ public ID3ReaderException(Throwable arg0) {
+ super(arg0);
+ }
+
+ public ID3ReaderException(String arg0, Throwable arg1) {
+ super(arg0, arg1);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java
new file mode 100644
index 000000000..89eab1398
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java
@@ -0,0 +1,17 @@
+package de.danoeh.antennapod.core.util.id3reader.model;
+
+public class FrameHeader extends Header {
+
+ protected char flags;
+
+ public FrameHeader(String id, int size, char flags) {
+ super(id, size);
+ this.flags = flags;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size));
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java
new file mode 100644
index 000000000..346e2893f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java
@@ -0,0 +1,29 @@
+package de.danoeh.antennapod.core.util.id3reader.model;
+
+public abstract class Header {
+
+ protected String id;
+ protected int size;
+
+ public Header(String id, int size) {
+ super();
+ this.id = id;
+ this.size = size;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ @Override
+ public String toString() {
+ return "Header [id=" + id + ", size=" + size + "]";
+ }
+
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java
new file mode 100644
index 000000000..0a6b8357f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.core.util.id3reader.model;
+
+public class TagHeader extends Header {
+
+ protected char version;
+ protected byte flags;
+
+ public TagHeader(String id, int size, char version, byte flags) {
+ super(id, size);
+ this.version = version;
+ this.flags = flags;
+ }
+
+ @Override
+ public String toString() {
+ return "TagHeader [version=" + version + ", flags=" + flags + ", id="
+ + id + ", size=" + size + "]";
+ }
+
+ public char getVersion() {
+ return version;
+ }
+
+
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java
new file mode 100644
index 000000000..aafcea307
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import com.aocate.media.MediaPlayer;
+
+public class AudioPlayer extends MediaPlayer implements IPlayer {
+ private static final String TAG = "AudioPlayer";
+
+ public AudioPlayer(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setScreenOnWhilePlaying(boolean screenOn) {
+ Log.e(TAG, "Setting screen on while playing not supported in Audio Player");
+ throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player");
+
+ }
+
+ @Override
+ public void setDisplay(SurfaceHolder sh) {
+ if (sh != null) {
+ Log.e(TAG, "Setting display not supported in Audio Player");
+ throw new UnsupportedOperationException("Setting display not supported in Audio Player");
+ }
+ }
+
+ @Override
+ public void setVideoScalingMode(int mode) {
+ throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player");
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
new file mode 100644
index 000000000..49769f4f0
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
@@ -0,0 +1,235 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.util.ChapterUtils;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/** Represents a media file that is stored on the local storage device. */
+public class ExternalMedia implements Playable {
+
+ public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2;
+ public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl";
+ public static final String PREF_POSITION = "ExternalMedia.PrefPosition";
+ public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType";
+
+ private String source;
+
+ private String episodeTitle;
+ private String feedTitle;
+ private MediaType mediaType = MediaType.AUDIO;
+ private List<Chapter> chapters;
+ private int duration;
+ private int position;
+
+ public ExternalMedia(String source, MediaType mediaType) {
+ super();
+ this.source = source;
+ this.mediaType = mediaType;
+ }
+
+ public ExternalMedia(String source, MediaType mediaType, int position) {
+ this(source, mediaType);
+ this.position = position;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(source);
+ dest.writeString(mediaType.toString());
+ dest.writeInt(position);
+ }
+
+ @Override
+ public void writeToPreferences(Editor prefEditor) {
+ prefEditor.putString(PREF_SOURCE_URL, source);
+ prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString());
+ prefEditor.putInt(PREF_POSITION, position);
+ }
+
+ @Override
+ public void loadMetadata() throws PlayableException {
+ MediaMetadataRetriever mmr = new MediaMetadataRetriever();
+ try {
+ mmr.setDataSource(source);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ throw new PlayableException(
+ "IllegalArgumentException when setting up MediaMetadataReceiver");
+ } catch (RuntimeException e) {
+ // http://code.google.com/p/android/issues/detail?id=39770
+ e.printStackTrace();
+ throw new PlayableException(
+ "RuntimeException when setting up MediaMetadataRetriever");
+ }
+ episodeTitle = mmr
+ .extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ feedTitle = mmr
+ .extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
+ try {
+ duration = Integer.parseInt(mmr
+ .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ throw new PlayableException("NumberFormatException when reading duration of media file");
+ }
+ ChapterUtils.loadChaptersFromFileUrl(this);
+ }
+
+ @Override
+ public void loadChapterMarks() {
+
+ }
+
+ @Override
+ public String getEpisodeTitle() {
+ return episodeTitle;
+ }
+
+ @Override
+ public Callable<String> loadShownotes() {
+ return new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ return "";
+ }
+ };
+ }
+
+ @Override
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+ @Override
+ public String getWebsiteLink() {
+ return null;
+ }
+
+ @Override
+ public String getPaymentLink() {
+ return null;
+ }
+
+ @Override
+ public String getFeedTitle() {
+ return feedTitle;
+ }
+
+ @Override
+ public Object getIdentifier() {
+ return source;
+ }
+
+ @Override
+ public int getDuration() {
+ return duration;
+ }
+
+ @Override
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public MediaType getMediaType() {
+ return mediaType;
+ }
+
+ @Override
+ public String getLocalMediaUrl() {
+ return source;
+ }
+
+ @Override
+ public String getStreamUrl() {
+ return null;
+ }
+
+ @Override
+ public boolean localFileAvailable() {
+ return true;
+ }
+
+ @Override
+ public boolean streamAvailable() {
+ return false;
+ }
+
+ @Override
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
+ SharedPreferences.Editor editor = pref.edit();
+ editor.putInt(PREF_POSITION, newPosition);
+ position = newPosition;
+ editor.commit();
+ }
+
+ @Override
+ public void setPosition(int newPosition) {
+ position = newPosition;
+ }
+
+ @Override
+ public void setDuration(int newDuration) {
+ duration = newDuration;
+ }
+
+ @Override
+ public void onPlaybackStart() {
+
+ }
+
+ @Override
+ public void onPlaybackCompleted() {
+
+ }
+
+ @Override
+ public int getPlayableType() {
+ return PLAYABLE_TYPE_EXTERNAL_MEDIA;
+ }
+
+ @Override
+ public void setChapters(List<Chapter> chapters) {
+ this.chapters = chapters;
+ }
+
+ public static final Parcelable.Creator<ExternalMedia> CREATOR = new Parcelable.Creator<ExternalMedia>() {
+ public ExternalMedia createFromParcel(Parcel in) {
+ String source = in.readString();
+ MediaType type = MediaType.valueOf(in.readString());
+ int position = 0;
+ if (in.dataAvail() > 0) {
+ position = in.readInt();
+ }
+ ExternalMedia extMedia = new ExternalMedia(source, type, position);
+ return extMedia;
+ }
+
+ public ExternalMedia[] newArray(int size) {
+ return new ExternalMedia[size];
+ }
+ };
+
+ @Override
+ public Uri getImageUri() {
+ if (localFileAvailable()) {
+ return new Uri.Builder().scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()).build();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java
new file mode 100644
index 000000000..147c7848d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+
+public interface IPlayer {
+ boolean canSetPitch();
+
+ boolean canSetSpeed();
+
+ float getCurrentPitchStepsAdjustment();
+
+ int getCurrentPosition();
+
+ float getCurrentSpeedMultiplier();
+
+ int getDuration();
+
+ float getMaxSpeedMultiplier();
+
+ float getMinSpeedMultiplier();
+
+ boolean isLooping();
+
+ boolean isPlaying();
+
+ void pause();
+
+ void prepare() throws IllegalStateException, IOException;
+
+ void prepareAsync();
+
+ void release();
+
+ void reset();
+
+ void seekTo(int msec);
+
+ void setAudioStreamType(int streamtype);
+
+ void setScreenOnWhilePlaying(boolean screenOn);
+
+ void setDataSource(String path) throws IllegalStateException, IOException,
+ IllegalArgumentException, SecurityException;
+
+ void setDisplay(SurfaceHolder sh);
+
+ void setEnableSpeedAdjustment(boolean enableSpeedAdjustment);
+
+ void setLooping(boolean looping);
+
+ void setPitchStepsAdjustment(float pitchSteps);
+
+ void setPlaybackPitch(float f);
+
+ void setPlaybackSpeed(float f);
+
+ void setVolume(float left, float right);
+
+ void start();
+
+ void stop();
+
+ public void setVideoScalingMode(int mode);
+
+ public void setWakeMode(Context context, int mode);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java
new file mode 100644
index 000000000..0650225f0
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java
@@ -0,0 +1,23 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import de.danoeh.antennapod.core.R;
+
+/** Utility class for MediaPlayer errors. */
+public class MediaPlayerError {
+
+ /** Get a human-readable string for a specific error code. */
+ public static String getErrorString(Context context, int code) {
+ int resId;
+ switch(code) {
+ case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
+ resId = R.string.playback_error_server_died;
+ break;
+ default:
+ resId = R.string.playback_error_unknown;
+ break;
+ }
+ return context.getString(resId);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
new file mode 100644
index 000000000..7ebd580f7
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
@@ -0,0 +1,207 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.List;
+
+import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.ShownotesProvider;
+
+/**
+ * Interface for objects that can be played by the PlaybackService.
+ */
+public interface Playable extends Parcelable,
+ ShownotesProvider, PicassoImageResource {
+
+ /**
+ * Save information about the playable in a preference so that it can be
+ * restored later via PlayableUtils.createInstanceFromPreferences.
+ * Implementations must NOT call commit() after they have written the values
+ * to the preferences file.
+ */
+ public void writeToPreferences(SharedPreferences.Editor prefEditor);
+
+ /**
+ * This method is called from a separate thread by the PlaybackService.
+ * Playable objects should load their metadata in this method. This method
+ * should execute as quickly as possible and NOT load chapter marks if no
+ * local file is available.
+ */
+ public void loadMetadata() throws PlayableException;
+
+ /**
+ * This method is called from a separate thread by the PlaybackService.
+ * Playable objects should load their chapter marks in this method if no
+ * local file was available when loadMetadata() was called.
+ */
+ public void loadChapterMarks();
+
+ /**
+ * Returns the title of the episode that this playable represents
+ */
+ public String getEpisodeTitle();
+
+ /**
+ * Returns a list of chapter marks or null if this Playable has no chapters.
+ */
+ public List<Chapter> getChapters();
+
+ /**
+ * Returns a link to a website that is meant to be shown in a browser
+ */
+ public String getWebsiteLink();
+
+ public String getPaymentLink();
+
+ /**
+ * Returns the title of the feed this Playable belongs to.
+ */
+ public String getFeedTitle();
+
+ /**
+ * Returns a unique identifier, for example a file url or an ID from a
+ * database.
+ */
+ public Object getIdentifier();
+
+ /**
+ * Return duration of object or 0 if duration is unknown.
+ */
+ public int getDuration();
+
+ /**
+ * Return position of object or 0 if position is unknown.
+ */
+ public int getPosition();
+
+ /**
+ * Returns the type of media. This method should return the correct value
+ * BEFORE loadMetadata() is called.
+ */
+ public MediaType getMediaType();
+
+ /**
+ * Returns an url to a local file that can be played or null if this file
+ * does not exist.
+ */
+ public String getLocalMediaUrl();
+
+ /**
+ * Returns an url to a file that can be streamed by the player or null if
+ * this url is not known.
+ */
+ public String getStreamUrl();
+
+ /**
+ * Returns true if a local file that can be played is available. getFileUrl
+ * MUST return a non-null string if this method returns true.
+ */
+ public boolean localFileAvailable();
+
+ /**
+ * Returns true if a streamable file is available. getStreamUrl MUST return
+ * a non-null string if this method returns true.
+ */
+ public boolean streamAvailable();
+
+ /**
+ * Saves the current position of this object. Implementations can use the
+ * provided SharedPreference to save this information and retrieve it later
+ * via PlayableUtils.createInstanceFromPreferences.
+ */
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition);
+
+ public void setPosition(int newPosition);
+
+ public void setDuration(int newDuration);
+
+ /**
+ * Is called by the PlaybackService when playback starts.
+ */
+ public void onPlaybackStart();
+
+ /**
+ * Is called by the PlaybackService when playback is completed.
+ */
+ public void onPlaybackCompleted();
+
+ /**
+ * Returns an integer that must be unique among all Playable classes. The
+ * return value is later used by PlayableUtils to determine the type of the
+ * Playable object that is restored.
+ */
+ public int getPlayableType();
+
+ public void setChapters(List<Chapter> chapters);
+
+ /**
+ * Provides utility methods for Playable objects.
+ */
+ public static class PlayableUtils {
+ private static final String TAG = "PlayableUtils";
+
+ /**
+ * Restores a playable object from a sharedPreferences file. This method might load data from the database,
+ * depending on the type of playable that was restored.
+ *
+ * @param type An integer that represents the type of the Playable object
+ * that is restored.
+ * @param pref The SharedPreferences file from which the Playable object
+ * is restored
+ * @return The restored Playable object
+ */
+ public static Playable createInstanceFromPreferences(Context context, int type,
+ SharedPreferences pref) {
+ // ADD new Playable types here:
+ switch (type) {
+ case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
+ long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
+ if (mediaId != -1) {
+ return DBReader.getFeedMedia(context, mediaId);
+ }
+ break;
+ case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
+ String source = pref.getString(ExternalMedia.PREF_SOURCE_URL,
+ null);
+ String mediaType = pref.getString(
+ ExternalMedia.PREF_MEDIA_TYPE, null);
+ if (source != null && mediaType != null) {
+ int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
+ return new ExternalMedia(source,
+ MediaType.valueOf(mediaType), position);
+ }
+ break;
+ }
+ Log.e(TAG, "Could not restore Playable object from preferences");
+ return null;
+ }
+ }
+
+ public static class PlayableException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PlayableException() {
+ super();
+ }
+
+ public PlayableException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public PlayableException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public PlayableException(Throwable throwable) {
+ super(throwable);
+ }
+
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
new file mode 100644
index 000000000..6b843e040
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -0,0 +1,790 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.app.Activity;
+import android.content.*;
+import android.content.res.TypedArray;
+import android.media.MediaPlayer;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.util.Pair;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils;
+
+import java.util.concurrent.*;
+
+/**
+ * Communicates with the playback service. GUI classes should use this class to
+ * control playback instead of communicating with the PlaybackService directly.
+ */
+public abstract class PlaybackController {
+ private static final String TAG = "PlaybackController";
+
+ public static final int INVALID_TIME = -1;
+
+ private final Activity activity;
+
+ private PlaybackService playbackService;
+ private Playable media;
+ private PlayerStatus status;
+
+ private ScheduledThreadPoolExecutor schedExecutor;
+ private static final int SCHED_EX_POOLSIZE = 1;
+
+ protected MediaPositionObserver positionObserver;
+ protected ScheduledFuture positionObserverFuture;
+
+ private boolean mediaInfoLoaded = false;
+ private boolean released = false;
+
+ /**
+ * True if controller should reinit playback service if 'pause' button is
+ * pressed.
+ */
+ private boolean reinitOnPause;
+
+ public PlaybackController(Activity activity, boolean reinitOnPause) {
+ Validate.notNull(activity);
+
+ this.activity = activity;
+ this.reinitOnPause = reinitOnPause;
+ schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE,
+ new ThreadFactory() {
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ }, new RejectedExecutionHandler() {
+
+ @Override
+ public void rejectedExecution(Runnable r,
+ ThreadPoolExecutor executor) {
+ Log.w(TAG,
+ "Rejected execution of runnable in schedExecutor");
+ }
+ }
+ );
+ }
+
+ /**
+ * Creates a new connection to the playbackService. Should be called in the
+ * activity's onResume() method.
+ */
+ public void init() {
+ activity.registerReceiver(statusUpdate, new IntentFilter(
+ PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
+
+ activity.registerReceiver(notificationReceiver, new IntentFilter(
+ PlaybackService.ACTION_PLAYER_NOTIFICATION));
+
+ activity.registerReceiver(shutdownReceiver, new IntentFilter(
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+
+ if (!released) {
+ bindToService();
+ } else {
+ throw new IllegalStateException(
+ "Can't call init() after release() has been called");
+ }
+ }
+
+ /**
+ * Should be called if the PlaybackController is no longer needed, for
+ * example in the activity's onStop() method.
+ */
+ public void release() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Releasing PlaybackController");
+
+ try {
+ activity.unregisterReceiver(statusUpdate);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ activity.unregisterReceiver(notificationReceiver);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ activity.unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ activity.unregisterReceiver(shutdownReceiver);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ cancelPositionObserver();
+ schedExecutor.shutdownNow();
+ media = null;
+ released = true;
+
+ }
+
+ /**
+ * Should be called in the activity's onPause() method.
+ */
+ public void pause() {
+ mediaInfoLoaded = false;
+ }
+
+ /**
+ * Tries to establish a connection to the PlaybackService. If it isn't
+ * running, the PlaybackService will be started with the last played media
+ * as the arguments of the launch intent.
+ */
+ private void bindToService() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Trying to connect to service");
+ AsyncTask<Void, Void, Intent> intentLoader = new AsyncTask<Void, Void, Intent>() {
+ @Override
+ protected Intent doInBackground(Void... voids) {
+ return getPlayLastPlayedMediaIntent();
+ }
+
+ @Override
+ protected void onPostExecute(Intent serviceIntent) {
+ boolean bound = false;
+ if (!PlaybackService.started) {
+ if (serviceIntent != null) {
+ if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service");
+ activity.startService(serviceIntent);
+ bound = activity.bindService(serviceIntent, mConnection, 0);
+ } else {
+ status = PlayerStatus.STOPPED;
+ setupGUI();
+ handleStatus();
+ }
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG,
+ "PlaybackService is running, trying to connect without start command.");
+ bound = activity.bindService(new Intent(activity,
+ PlaybackService.class), mConnection, 0);
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Result for service binding: " + bound);
+ }
+ };
+ intentLoader.execute();
+ }
+
+ /**
+ * Returns an intent that starts the PlaybackService and plays the last
+ * played media or null if no last played media could be found.
+ */
+ private Intent getPlayLastPlayedMediaIntent() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Trying to restore last played media");
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(activity.getApplicationContext());
+ long currentlyPlayingMedia = PlaybackPreferences
+ .getCurrentlyPlayingMedia();
+ if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
+ Playable media = PlayableUtils.createInstanceFromPreferences(activity,
+ (int) currentlyPlayingMedia, prefs);
+ if (media != null) {
+ Intent serviceIntent = new Intent(activity,
+ PlaybackService.class);
+ serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
+ serviceIntent.putExtra(
+ PlaybackService.EXTRA_START_WHEN_PREPARED, false);
+ serviceIntent.putExtra(
+ PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false);
+ boolean fileExists = media.localFileAvailable();
+ boolean lastIsStream = PlaybackPreferences
+ .getCurrentEpisodeIsStream();
+ if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
+ DBTasks.notifyMissingFeedMediaFile(
+ activity, (FeedMedia) media);
+ }
+ serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
+ lastIsStream || !fileExists);
+ return serviceIntent;
+ }
+ }
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "No last played media found");
+ return null;
+ }
+
+ public abstract void setupGUI();
+
+ private void setupPositionObserver() {
+ if ((positionObserverFuture != null && positionObserverFuture
+ .isCancelled())
+ || (positionObserverFuture != null && positionObserverFuture
+ .isDone()) || positionObserverFuture == null) {
+
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Setting up position observer");
+ positionObserver = new MediaPositionObserver();
+ positionObserverFuture = schedExecutor.scheduleWithFixedDelay(
+ positionObserver, MediaPositionObserver.WAITING_INTERVALL,
+ MediaPositionObserver.WAITING_INTERVALL,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private void cancelPositionObserver() {
+ if (positionObserverFuture != null) {
+ boolean result = positionObserverFuture.cancel(true);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "PositionObserver cancelled. Result: " + result);
+ }
+ }
+
+ public abstract void onPositionObserverUpdate();
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ playbackService = ((PlaybackService.LocalBinder) service)
+ .getService();
+ if (!released) {
+ queryService();
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Connection to Service established");
+ } else {
+ Log.i(TAG, "Connection to playback service has been established, but controller has already been released");
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ playbackService = null;
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Disconnected from Service");
+
+ }
+ };
+
+ protected BroadcastReceiver statusUpdate = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Received statusUpdate Intent.");
+ if (isConnectedToPlaybackService()) {
+ PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo();
+ status = info.playerStatus;
+ media = info.playable;
+ handleStatus();
+ } else {
+ Log.w(TAG,
+ "Couldn't receive status update: playbackService was null");
+ bindToService();
+ }
+ }
+ };
+
+ protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (isConnectedToPlaybackService()) {
+ int type = intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_TYPE, -1);
+ int code = intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_CODE, -1);
+ if (code != -1 && type != -1) {
+ switch (type) {
+ case PlaybackService.NOTIFICATION_TYPE_ERROR:
+ handleError(code);
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE:
+ float progress = ((float) code) / 100;
+ onBufferUpdate(progress);
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_RELOAD:
+ cancelPositionObserver();
+ mediaInfoLoaded = false;
+ queryService();
+ onReloadNotification(intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_CODE, -1));
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE:
+ onSleepTimerUpdate();
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_BUFFER_START:
+ onBufferStart();
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_BUFFER_END:
+ onBufferEnd();
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END:
+ onPlaybackEnd();
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE:
+ onPlaybackSpeedChange();
+ break;
+ }
+
+ } else {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Bad arguments. Won't handle intent");
+ }
+ } else {
+ bindToService();
+ }
+ }
+
+ };
+
+ private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (isConnectedToPlaybackService()) {
+ if (StringUtils.equals(intent.getAction(),
+ PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ release();
+ onShutdownNotification();
+ }
+ }
+ }
+ };
+
+ public abstract void onPlaybackSpeedChange();
+
+ public abstract void onShutdownNotification();
+
+ /**
+ * Called when the currently displayed information should be refreshed.
+ */
+ public abstract void onReloadNotification(int code);
+
+ public abstract void onBufferStart();
+
+ public abstract void onBufferEnd();
+
+ public abstract void onBufferUpdate(float progress);
+
+ public abstract void onSleepTimerUpdate();
+
+ public abstract void handleError(int code);
+
+ public abstract void onPlaybackEnd();
+
+ public void repeatHandleStatus() {
+ if (status != null && playbackService != null) {
+ handleStatus();
+ }
+ }
+
+ /**
+ * Is called whenever the PlaybackService changes it's status. This method
+ * should be used to update the GUI or start/cancel background threads.
+ */
+ private void handleStatus() {
+ final int playResource;
+ final int pauseResource;
+ final CharSequence playText = activity.getString(R.string.play_label);
+ final CharSequence pauseText = activity.getString(R.string.pause_label);
+
+ if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) {
+ TypedArray res = activity.obtainStyledAttributes(new int[]{
+ R.attr.av_play, R.attr.av_pause});
+ playResource = res.getResourceId(0, R.drawable.av_play);
+ pauseResource = res.getResourceId(1, R.drawable.av_pause);
+ res.recycle();
+ } else {
+ playResource = R.drawable.ic_action_play_over_video;
+ pauseResource = R.drawable.ic_action_pause_over_video;
+ }
+
+ switch (status) {
+
+ case ERROR:
+ postStatusMsg(R.string.player_error_msg);
+ handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN);
+ break;
+ case PAUSED:
+ clearStatusMsg();
+ checkMediaInfoLoaded();
+ cancelPositionObserver();
+ updatePlayButtonAppearance(playResource, playText);
+ if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
+ setScreenOn(false);
+ }
+ break;
+ case PLAYING:
+ clearStatusMsg();
+ checkMediaInfoLoaded();
+ if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
+ onAwaitingVideoSurface();
+ setScreenOn(true);
+ }
+ setupPositionObserver();
+ updatePlayButtonAppearance(pauseResource, pauseText);
+ break;
+ case PREPARING:
+ postStatusMsg(R.string.player_preparing_msg);
+ checkMediaInfoLoaded();
+ if (playbackService != null) {
+ if (playbackService.isStartWhenPrepared()) {
+ updatePlayButtonAppearance(pauseResource, pauseText);
+ } else {
+ updatePlayButtonAppearance(playResource, playText);
+ }
+ }
+ break;
+ case STOPPED:
+ postStatusMsg(R.string.player_stopped_msg);
+ break;
+ case PREPARED:
+ checkMediaInfoLoaded();
+ postStatusMsg(R.string.player_ready_msg);
+ updatePlayButtonAppearance(playResource, playText);
+ break;
+ case SEEKING:
+ postStatusMsg(R.string.player_seeking_msg);
+ break;
+ case INITIALIZED:
+ checkMediaInfoLoaded();
+ clearStatusMsg();
+ updatePlayButtonAppearance(playResource, playText);
+ break;
+ }
+ }
+
+ private void checkMediaInfoLoaded() {
+ mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo());
+ }
+
+ private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) {
+ ImageButton butPlay = getPlayButton();
+ butPlay.setImageResource(resource);
+ butPlay.setContentDescription(contentDescription);
+ }
+
+ public abstract ImageButton getPlayButton();
+
+ public abstract void postStatusMsg(int msg);
+
+ public abstract void clearStatusMsg();
+
+ public abstract boolean loadMediaInfo();
+
+ public abstract void onAwaitingVideoSurface();
+
+ /**
+ * Called when connection to playback service has been established or
+ * information has to be refreshed
+ */
+ void queryService() {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Querying service info");
+ if (playbackService != null) {
+ status = playbackService.getStatus();
+ media = playbackService.getPlayable();
+ /*
+ if (media == null) {
+ Log.w(TAG,
+ "PlaybackService has no media object. Trying to restore last played media.");
+ Intent serviceIntent = getPlayLastPlayedMediaIntent();
+ if (serviceIntent != null) {
+ activity.startService(serviceIntent);
+ }
+ }
+ */
+ onServiceQueried();
+
+ setupGUI();
+ handleStatus();
+ // make sure that new media is loaded if it's available
+ mediaInfoLoaded = false;
+
+ } else {
+ Log.e(TAG,
+ "queryService() was called without an existing connection to playbackservice");
+ }
+ }
+
+ public abstract void onServiceQueried();
+
+ /**
+ * Should be used by classes which implement the OnSeekBarChanged interface.
+ */
+ public float onSeekBarProgressChanged(SeekBar seekBar, int progress,
+ boolean fromUser, TextView txtvPosition) {
+ if (fromUser && playbackService != null && media != null) {
+ float prog = progress / ((float) seekBar.getMax());
+ int duration = media.getDuration();
+ txtvPosition.setText(Converter
+ .getDurationStringLong((int) (prog * duration)));
+ return prog;
+ }
+ return 0;
+
+ }
+
+ /**
+ * Should be used by classes which implement the OnSeekBarChanged interface.
+ */
+ public void onSeekBarStartTrackingTouch(SeekBar seekBar) {
+ // interrupt position Observer, restart later
+ cancelPositionObserver();
+ }
+
+ /**
+ * Should be used by classes which implement the OnSeekBarChanged interface.
+ */
+ public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) {
+ if (playbackService != null) {
+ playbackService.seekTo((int) (prog * media.getDuration()));
+ setupPositionObserver();
+ }
+ }
+
+ /**
+ * Should be implemented by classes that show a video. The default implementation
+ * does nothing
+ *
+ * @param enable True if the screen should be kept on, false otherwise
+ */
+ protected void setScreenOn(boolean enable) {
+
+ }
+
+ public OnClickListener newOnPlayButtonClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (playbackService != null) {
+ switch (status) {
+ case PLAYING:
+ playbackService.pause(true, reinitOnPause);
+ break;
+ case PAUSED:
+ case PREPARED:
+ playbackService.resume();
+ break;
+ case PREPARING:
+ playbackService.setStartWhenPrepared(!playbackService
+ .isStartWhenPrepared());
+ if (reinitOnPause
+ && playbackService.isStartWhenPrepared() == false) {
+ playbackService.reinit();
+ }
+ break;
+ case INITIALIZED:
+ playbackService.setStartWhenPrepared(true);
+ playbackService.prepare();
+ break;
+ }
+ } else {
+ Log.w(TAG,
+ "Play/Pause button was pressed, but playbackservice was null!");
+ }
+ }
+
+ };
+ }
+
+ public OnClickListener newOnRevButtonClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (status == PlayerStatus.PLAYING) {
+ playbackService.seekDelta(-UserPreferences.getSeekDeltaMs());
+ }
+ }
+ };
+ }
+
+ public OnClickListener newOnFFButtonClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (status == PlayerStatus.PLAYING) {
+ playbackService.seekDelta(UserPreferences.getSeekDeltaMs());
+ }
+ }
+ };
+ }
+
+ public boolean serviceAvailable() {
+ return playbackService != null;
+ }
+
+ public int getPosition() {
+ if (playbackService != null) {
+ return playbackService.getCurrentPosition();
+ } else {
+ return PlaybackService.INVALID_TIME;
+ }
+ }
+
+ public int getDuration() {
+ if (playbackService != null) {
+ return playbackService.getDuration();
+ } else {
+ return PlaybackService.INVALID_TIME;
+ }
+ }
+
+ public Playable getMedia() {
+ return media;
+ }
+
+ public boolean sleepTimerActive() {
+ return playbackService != null && playbackService.sleepTimerActive();
+ }
+
+ public boolean sleepTimerNotActive() {
+ return playbackService != null && !playbackService.sleepTimerActive();
+ }
+
+ public void disableSleepTimer() {
+ if (playbackService != null) {
+ playbackService.disableSleepTimer();
+ }
+ }
+
+ public long getSleepTimerTimeLeft() {
+ if (playbackService != null) {
+ return playbackService.getSleepTimerTimeLeft();
+ } else {
+ return INVALID_TIME;
+ }
+ }
+
+ public void setSleepTimer(long time) {
+ if (playbackService != null) {
+ playbackService.setSleepTimer(time);
+ }
+ }
+
+ public void seekToChapter(Chapter chapter) {
+ if (playbackService != null) {
+ playbackService.seekToChapter(chapter);
+ }
+ }
+
+ public void seekTo(int time) {
+ if (playbackService != null) {
+ playbackService.seekTo(time);
+ }
+ }
+
+ public void setVideoSurface(SurfaceHolder holder) {
+ if (playbackService != null) {
+ playbackService.setVideoSurface(holder);
+ }
+ }
+
+ public PlayerStatus getStatus() {
+ return status;
+ }
+
+ public boolean canSetPlaybackSpeed() {
+ return playbackService != null && playbackService.canSetSpeed();
+ }
+
+ public void setPlaybackSpeed(float speed) {
+ if (playbackService != null) {
+ playbackService.setSpeed(speed);
+ }
+ }
+
+ public float getCurrentPlaybackSpeedMultiplier() {
+ if (canSetPlaybackSpeed()) {
+ return playbackService.getCurrentPlaybackSpeed();
+ } else {
+ return -1;
+ }
+ }
+
+ public boolean isPlayingVideo() {
+ if (playbackService != null) {
+ return PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
+ }
+ return false;
+ }
+
+ public Pair<Integer, Integer> getVideoSize() {
+ if (playbackService != null) {
+ return playbackService.getVideoSize();
+ } else {
+ return null;
+ }
+ }
+
+
+ /**
+ * Returns true if PlaybackController can communicate with the playback
+ * service.
+ */
+ public boolean isConnectedToPlaybackService() {
+ return playbackService != null;
+ }
+
+ public void notifyVideoSurfaceAbandoned() {
+ if (playbackService != null) {
+ playbackService.notifyVideoSurfaceAbandoned();
+ }
+ }
+
+ /**
+ * Move service into INITIALIZED state if it's paused to save bandwidth
+ */
+ public void reinitServiceIfPaused() {
+ if (playbackService != null
+ && playbackService.isStreaming()
+ && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService
+ .getStatus() == PlayerStatus.PREPARING && playbackService
+ .isStartWhenPrepared() == false))) {
+ playbackService.reinit();
+ }
+ }
+
+ /**
+ * Refreshes the current position of the media file that is playing.
+ */
+ public class MediaPositionObserver implements Runnable {
+
+ public static final int WAITING_INTERVALL = 1000;
+
+ @Override
+ public void run() {
+ if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) {
+ activity.runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ onPositionObserverUpdate();
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java
new file mode 100644
index 000000000..443ff0ad1
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java
@@ -0,0 +1,161 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.Log;
+import android.util.TypedValue;
+
+import org.apache.commons.lang3.Validate;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.ShownotesProvider;
+
+/**
+ * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the
+ * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's
+ * shownotes.
+ * <p/>
+ * A timeline object needs a shownotesProvider from which the chapter information is retrieved and shownotes are generated.
+ */
+public class Timeline {
+ private static final String TAG = "Timeline";
+
+ private static final String WEBVIEW_STYLE = "@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; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }";
+
+
+ private ShownotesProvider shownotesProvider;
+
+
+ private final String colorString;
+ private final int pageMargin;
+
+ public Timeline(Context context, ShownotesProvider shownotesProvider) {
+ if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null");
+ this.shownotesProvider = shownotesProvider;
+
+ TypedArray res = context
+ .getTheme()
+ .obtainStyledAttributes(
+ new int[]{android.R.attr.textColorPrimary});
+ int colorResource = res.getColor(0, 0);
+ colorString = String.format("#%06X",
+ 0xFFFFFF & colorResource);
+ res.recycle();
+
+ pageMargin = (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 8, context.getResources()
+ .getDisplayMetrics()
+ );
+ }
+
+ private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))");
+ private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"antennapod://timecode/%d\">%s</a>";
+ private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b");
+
+ /**
+ * Applies an app-specific CSS stylesheet and adds timecode links (optional).
+ * <p/>
+ * This method does NOT change the original shownotes string of the shownotesProvider object and it should
+ * also not be changed by the caller.
+ *
+ * @param addTimecodes True if this method should add timecode links
+ * @return The processed HTML string.
+ */
+ public String processShownotes(final boolean addTimecodes) {
+ final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null;
+
+ // load shownotes
+
+ String shownotes;
+ try {
+ shownotes = shownotesProvider.loadShownotes().call();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ if (shownotes == null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string");
+ return "";
+ }
+
+ Document document = Jsoup.parse(shownotes);
+
+ // apply style
+ String styleStr = String.format(WEBVIEW_STYLE, colorString, "100%", pageMargin,
+ pageMargin, pageMargin, pageMargin);
+ document.head().appendElement("style").attr("type", "text/css").text(styleStr);
+
+ // apply timecode links
+ if (addTimecodes) {
+ Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes");
+ for (Element element : elementsWithTimeCodes) {
+ Matcher matcherLong = TIMECODE_REGEX.matcher(element.text());
+ StringBuffer buffer = new StringBuffer();
+ while (matcherLong.find()) {
+ String h = matcherLong.group(1);
+ String group = matcherLong.group(0);
+ int time = (h != null) ? Converter.durationStringLongToMs(group) :
+ Converter.durationStringShortToMs(group);
+
+ String rep;
+ if (playable == null || playable.getDuration() > time) {
+ rep = String.format(TIMECODE_LINK, time, group);
+ } else {
+ rep = group;
+ }
+ matcherLong.appendReplacement(buffer, rep);
+ }
+ matcherLong.appendTail(buffer);
+
+ element.html(buffer.toString());
+ }
+ }
+
+ Log.i(TAG, "Out: " + document.toString());
+ return document.toString();
+ }
+
+
+ /**
+ * Returns true if the given link is a timecode link.
+ */
+ public static boolean isTimecodeLink(String link) {
+ return link != null && link.matches(TIMECODE_LINK_REGEX.pattern());
+ }
+
+ /**
+ * Returns the time in milliseconds that is attached to this link or -1
+ * if the link is no valid timecode link.
+ */
+ public static int getTimecodeLinkTime(String link) {
+ if (isTimecodeLink(link)) {
+ Matcher m = TIMECODE_LINK_REGEX.matcher(link);
+
+ try {
+ if (m.find()) {
+ return Integer.valueOf(m.group(1));
+ }
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ return -1;
+ }
+
+
+ public void setShownotesProvider(ShownotesProvider shownotesProvider) {
+ Validate.notNull(shownotesProvider);
+ this.shownotesProvider = shownotesProvider;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java
new file mode 100644
index 000000000..dc5270d8f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.media.MediaPlayer;
+import android.util.Log;
+
+public class VideoPlayer extends MediaPlayer implements IPlayer {
+ private static final String TAG = "VideoPlayer";
+
+ @Override
+ public boolean canSetPitch() {
+ return false;
+ }
+
+ @Override
+ public boolean canSetSpeed() {
+ return false;
+ }
+
+ @Override
+ public float getCurrentPitchStepsAdjustment() {
+ return 1;
+ }
+
+ @Override
+ public float getCurrentSpeedMultiplier() {
+ return 1;
+ }
+
+ @Override
+ public float getMaxSpeedMultiplier() {
+ return 1;
+ }
+
+ @Override
+ public float getMinSpeedMultiplier() {
+ return 1;
+ }
+
+ @Override
+ public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException {
+ Log.e(TAG, "Setting enable speed adjustment unsupported in video player");
+ throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player");
+ }
+
+ @Override
+ public void setPitchStepsAdjustment(float pitchSteps) {
+ Log.e(TAG, "Setting pitch steps adjustment unsupported in video player");
+ throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player");
+ }
+
+ @Override
+ public void setPlaybackPitch(float f) {
+ Log.e(TAG, "Setting playback pitch unsupported in video player");
+ throw new UnsupportedOperationException("Setting playback pitch unsupported in video player");
+ }
+
+ @Override
+ public void setPlaybackSpeed(float f) {
+ Log.e(TAG, "Setting playback speed unsupported in video player");
+ throw new UnsupportedOperationException("Setting playback speed unsupported in video player");
+ }
+
+ @Override
+ public void setVideoScalingMode(int mode) {
+ super.setVideoScalingMode(mode);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java
new file mode 100644
index 000000000..9588265b8
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java
@@ -0,0 +1,78 @@
+package de.danoeh.antennapod.core.util.syndication;
+
+import android.net.Uri;
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here:
+ * <p/>
+ * http://www.rssboard.org/rss-autodiscovery
+ * <p/>
+ * http://blog.whatwg.org/feed-autodiscovery
+ */
+public class FeedDiscoverer {
+
+ private static final String MIME_RSS = "application/rss+xml";
+ private static final String MIME_ATOM = "application/atom+xml";
+
+ /**
+ * Discovers links to RSS and Atom feeds in the given File which must be a HTML document.
+ *
+ * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if
+ * a title cannot be found).
+ */
+ public Map<String, String> findLinks(File in, String baseUrl) throws IOException {
+ return findLinks(Jsoup.parse(in, null), baseUrl);
+ }
+
+ /**
+ * Discovers links to RSS and Atom feeds in the given File which must be a HTML document.
+ *
+ * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if
+ * a title cannot be found).
+ */
+ public Map<String, String> findLinks(String in, String baseUrl) throws IOException {
+ return findLinks(Jsoup.parse(in), baseUrl);
+ }
+
+ private Map<String, String> findLinks(Document document, String baseUrl) {
+ Map<String, String> res = new LinkedHashMap<String, String>();
+ Elements links = document.head().getElementsByTag("link");
+ for (Element link : links) {
+ String rel = link.attr("rel");
+ String href = link.attr("href");
+ if (!StringUtils.isEmpty(href) &&
+ (rel.equals("alternate") || rel.equals("feed"))) {
+ String type = link.attr("type");
+ if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) {
+ String title = link.attr("title");
+ String processedUrl = processURL(baseUrl, href);
+ if (processedUrl != null) {
+ res.put(processedUrl,
+ (StringUtils.isEmpty(title)) ? href : title);
+ }
+ }
+ }
+ }
+ return res;
+ }
+
+ private String processURL(String baseUrl, String strUrl) {
+ Uri uri = Uri.parse(strUrl);
+ if (uri.isRelative()) {
+ Uri res = Uri.parse(baseUrl).buildUpon().path(strUrl).build();
+ return (res != null) ? res.toString() : null;
+ } else {
+ return strUrl;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java
new file mode 100644
index 000000000..4799d3881
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java
@@ -0,0 +1,81 @@
+package de.danoeh.antennapod.core.util.vorbiscommentreader;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+public class OggInputStream extends InputStream {
+ private InputStream input;
+
+ /** True if OggInputStream is currently inside an Ogg page. */
+ private boolean isInPage;
+ private long bytesLeft;
+
+ public OggInputStream(InputStream input) {
+ super();
+ isInPage = false;
+ this.input = input;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (!isInPage) {
+ readOggPage();
+ }
+
+ if (isInPage && bytesLeft > 0) {
+ int result = input.read();
+ bytesLeft -= 1;
+ if (bytesLeft == 0) {
+ isInPage = false;
+ }
+ return result;
+ }
+ return -1;
+ }
+
+ private void readOggPage() throws IOException {
+ // find OggS
+ int[] buffer = new int[4];
+ int c = 0;
+ boolean isInOggS = false;
+ while ((c = input.read()) != -1) {
+ switch (c) {
+ case 'O':
+ isInOggS = true;
+ buffer[0] = c;
+ break;
+ case 'g':
+ if (buffer[1] != c) {
+ buffer[1] = c;
+ } else {
+ buffer[2] = c;
+ }
+ break;
+ case 'S':
+ buffer[3] = c;
+ break;
+ default:
+ if (isInOggS) {
+ Arrays.fill(buffer, 0);
+ isInOggS = false;
+ }
+ }
+ if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g'
+ && buffer[3] == 'S') {
+ break;
+ }
+ }
+ // read segments
+ IOUtils.skipFully(input, 22);
+ bytesLeft = 0;
+ int numSegments = input.read();
+ for (int i = 0; i < numSegments; i++) {
+ bytesLeft += input.read();
+ }
+ isInPage = true;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java
new file mode 100644
index 000000000..c4961a3ab
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java
@@ -0,0 +1,101 @@
+package de.danoeh.antennapod.core.util.vorbiscommentreader;
+
+import android.util.Log;
+import de.danoeh.antennapod.core.BuildConfig;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.VorbisCommentChapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class VorbisCommentChapterReader extends VorbisCommentReader {
+ private static final String TAG = "VorbisCommentChapterReader";
+
+ private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*";
+ private static final String CHAPTER_ATTRIBUTE_TITLE = "name";
+ private static final String CHAPTER_ATTRIBUTE_LINK = "url";
+
+ private List<Chapter> chapters;
+
+ public VorbisCommentChapterReader() {
+ }
+
+ @Override
+ public void onVorbisCommentFound() {
+ System.out.println("Vorbis comment found");
+ }
+
+ @Override
+ public void onVorbisCommentHeaderFound(VorbisCommentHeader header) {
+ chapters = new ArrayList<Chapter>();
+ System.out.println(header.toString());
+ }
+
+ @Override
+ public boolean onContentVectorKey(String content) {
+ return content.matches(CHAPTER_KEY);
+ }
+
+ @Override
+ public void onContentVectorValue(String key, String value)
+ throws VorbisCommentReaderException {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Key: " + key + ", value: " + value);
+ String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key);
+ int id = VorbisCommentChapter.getIDFromKey(key);
+ Chapter chapter = getChapterById(id);
+ if (attribute == null) {
+ if (getChapterById(id) == null) {
+ // new chapter
+ long start = VorbisCommentChapter.getStartTimeFromValue(value);
+ chapter = new VorbisCommentChapter(id);
+ chapter.setStart(start);
+ chapters.add(chapter);
+ } else {
+ throw new VorbisCommentReaderException(
+ "Found chapter with duplicate ID (" + key + ", "
+ + value + ")");
+ }
+ } else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) {
+ if (chapter != null) {
+ chapter.setTitle(value);
+ }
+ } else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) {
+ if (chapter != null) {
+ chapter.setLink(value);
+ }
+ }
+ }
+
+ @Override
+ public void onNoVorbisCommentFound() {
+ System.out.println("No vorbis comment found");
+ }
+
+ @Override
+ public void onEndOfComment() {
+ System.out.println("End of comment");
+ for (Chapter c : chapters) {
+ System.out.println(c.toString());
+ }
+ }
+
+ @Override
+ public void onError(VorbisCommentReaderException exception) {
+ exception.printStackTrace();
+ }
+
+ private Chapter getChapterById(long id) {
+ for (Chapter c : chapters) {
+ if (((VorbisCommentChapter) c).getVorbisCommentId() == id) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java
new file mode 100644
index 000000000..5f9dd0faf
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.core.util.vorbiscommentreader;
+public class VorbisCommentHeader {
+ private String vendorString;
+ private long userCommentLength;
+
+ public VorbisCommentHeader(String vendorString, long userCommentLength) {
+ super();
+ this.vendorString = vendorString;
+ this.userCommentLength = userCommentLength;
+ }
+
+ @Override
+ public String toString() {
+ return "VorbisCommentHeader [vendorString=" + vendorString
+ + ", userCommentLength=" + userCommentLength + "]";
+ }
+
+ public String getVendorString() {
+ return vendorString;
+ }
+
+ public long getUserCommentLength() {
+ return userCommentLength;
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java
new file mode 100644
index 000000000..9639b9c42
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java
@@ -0,0 +1,194 @@
+package de.danoeh.antennapod.core.util.vorbiscommentreader;
+
+import org.apache.commons.io.EndianUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+
+public abstract class VorbisCommentReader {
+ /** Length of first page in an ogg file in bytes. */
+ private static final int FIRST_PAGE_LENGTH = 58;
+ private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024;
+ private static final int PACKET_TYPE_IDENTIFICATION = 1;
+ private static final int PACKET_TYPE_COMMENT = 3;
+
+ /** Called when Reader finds identification header. */
+ public abstract void onVorbisCommentFound();
+
+ public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header);
+
+ /**
+ * Is called every time the Reader finds a content vector. The handler
+ * should return true if it wants to handle the content vector.
+ */
+ public abstract boolean onContentVectorKey(String content);
+
+ /**
+ * Is called if onContentVectorKey returned true for the key.
+ *
+ * @throws VorbisCommentReaderException
+ */
+ public abstract void onContentVectorValue(String key, String value)
+ throws VorbisCommentReaderException;
+
+ public abstract void onNoVorbisCommentFound();
+
+ public abstract void onEndOfComment();
+
+ public abstract void onError(VorbisCommentReaderException exception);
+
+ public void readInputStream(InputStream input)
+ throws VorbisCommentReaderException {
+ try {
+ // look for identification header
+ if (findIdentificationHeader(input)) {
+
+ onVorbisCommentFound();
+ input = new OggInputStream(input);
+ if (findCommentHeader(input)) {
+ VorbisCommentHeader commentHeader = readCommentHeader(input);
+ if (commentHeader != null) {
+ onVorbisCommentHeaderFound(commentHeader);
+ for (int i = 0; i < commentHeader
+ .getUserCommentLength(); i++) {
+ try {
+ long vectorLength = EndianUtils
+ .readSwappedUnsignedInteger(input);
+ String key = readContentVectorKey(input,
+ vectorLength).toLowerCase();
+ boolean readValue = onContentVectorKey(key);
+ if (readValue) {
+ String value = readUTF8String(
+ input,
+ (int) (vectorLength - key.length() - 1));
+ onContentVectorValue(key, value);
+ } else {
+ IOUtils.skipFully(input,
+ vectorLength - key.length() - 1);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ onEndOfComment();
+ }
+
+ } else {
+ onError(new VorbisCommentReaderException(
+ "No comment header found"));
+ }
+ } else {
+ onNoVorbisCommentFound();
+ }
+ } catch (IOException e) {
+ onError(new VorbisCommentReaderException(e));
+ }
+ }
+
+ private String readUTF8String(InputStream input, long length)
+ throws IOException {
+ byte[] buffer = new byte[(int) length];
+
+ IOUtils.readFully(input, buffer);
+ Charset charset = Charset.forName("UTF-8");
+ return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString();
+ }
+
+ /**
+ * Looks for an identification header in the first page of the file. If an
+ * identification header is found, it will be skipped completely and the
+ * method will return true, otherwise false.
+ *
+ * @throws IOException
+ */
+ private boolean findIdentificationHeader(InputStream input)
+ throws IOException {
+ byte[] buffer = new byte[FIRST_PAGE_LENGTH];
+ IOUtils.readFully(input, buffer);
+ int i;
+ for (i = 6; i < buffer.length; i++) {
+ if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o'
+ && buffer[i - 3] == 'r' && buffer[i - 2] == 'b'
+ && buffer[i - 1] == 'i' && buffer[i] == 's'
+ && buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean findCommentHeader(InputStream input) throws IOException {
+ char[] buffer = new char["vorbis".length() + 1];
+ for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
+ char c = (char) input.read();
+ int dest = -1;
+ switch (c) {
+ case PACKET_TYPE_COMMENT:
+ dest = 0;
+ break;
+ case 'v':
+ dest = 1;
+ break;
+ case 'o':
+ dest = 2;
+ break;
+ case 'r':
+ dest = 3;
+ break;
+ case 'b':
+ dest = 4;
+ break;
+ case 'i':
+ dest = 5;
+ break;
+ case 's':
+ dest = 6;
+ break;
+ }
+ if (dest >= 0) {
+ buffer[dest] = c;
+ if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r'
+ && buffer[4] == 'b' && buffer[5] == 'i'
+ && buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) {
+ return true;
+ }
+ } else {
+ Arrays.fill(buffer, (char) 0);
+ }
+ }
+ return false;
+ }
+
+ private VorbisCommentHeader readCommentHeader(InputStream input)
+ throws IOException, VorbisCommentReaderException {
+ try {
+ long vendorLength = EndianUtils.readSwappedUnsignedInteger(input);
+ String vendorName = readUTF8String(input, vendorLength);
+ long userCommentLength = EndianUtils
+ .readSwappedUnsignedInteger(input);
+ return new VorbisCommentHeader(vendorName, userCommentLength);
+ } catch (UnsupportedEncodingException e) {
+ throw new VorbisCommentReaderException(e);
+ }
+ }
+
+ private String readContentVectorKey(InputStream input, long vectorLength)
+ throws IOException {
+ StringBuffer buffer = new StringBuffer();
+ for (int i = 0; i < vectorLength; i++) {
+ char c = (char) input.read();
+ if (c == '=') {
+ return buffer.toString();
+ } else {
+ buffer.append(c);
+ }
+ }
+ return null; // no key found
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java
new file mode 100644
index 000000000..89ab20db0
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java
@@ -0,0 +1,24 @@
+package de.danoeh.antennapod.core.util.vorbiscommentreader;
+public class VorbisCommentReaderException extends Exception {
+
+ public VorbisCommentReaderException() {
+ super();
+ // TODO Auto-generated constructor stub
+ }
+
+ public VorbisCommentReaderException(String arg0, Throwable arg1) {
+ super(arg0, arg1);
+ // TODO Auto-generated constructor stub
+ }
+
+ public VorbisCommentReaderException(String arg0) {
+ super(arg0);
+ // TODO Auto-generated constructor stub
+ }
+
+ public VorbisCommentReaderException(Throwable arg0) {
+ super(arg0);
+ // TODO Auto-generated constructor stub
+ }
+
+}
diff --git a/res/drawable-hdpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna_default.png
index 37d73c734..37d73c734 100644
--- a/res/drawable-hdpi-v11/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-hdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png
index ad148cc6b..ad148cc6b 100755
--- a/res/drawable-hdpi-v11/ic_stat_authentication.png
+++ b/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png
Binary files differ
diff --git a/res/drawable-hdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png
index 90b39c958..90b39c958 100644
--- a/res/drawable-hdpi-v11/stat_notify_sync.png
+++ b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png
Binary files differ
diff --git a/res/drawable-hdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png
index 074cdee27..074cdee27 100644
--- a/res/drawable-hdpi-v11/stat_notify_sync_error.png
+++ b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png
Binary files differ
diff --git a/res/drawable-hdpi/action_about.png b/core/src/main/res/drawable-hdpi/action_about.png
index 8f39c428a..8f39c428a 100644
--- a/res/drawable-hdpi/action_about.png
+++ b/core/src/main/res/drawable-hdpi/action_about.png
Binary files differ
diff --git a/res/drawable-hdpi/action_about_dark.png b/core/src/main/res/drawable-hdpi/action_about_dark.png
index 6eaf08aec..6eaf08aec 100755
--- a/res/drawable-hdpi/action_about_dark.png
+++ b/core/src/main/res/drawable-hdpi/action_about_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/action_search.png b/core/src/main/res/drawable-hdpi/action_search.png
index e6b704518..e6b704518 100644
--- a/res/drawable-hdpi/action_search.png
+++ b/core/src/main/res/drawable-hdpi/action_search.png
Binary files differ
diff --git a/res/drawable-hdpi/action_search_dark.png b/core/src/main/res/drawable-hdpi/action_search_dark.png
index f12e005eb..f12e005eb 100755
--- a/res/drawable-hdpi/action_search_dark.png
+++ b/core/src/main/res/drawable-hdpi/action_search_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/action_settings.png b/core/src/main/res/drawable-hdpi/action_settings.png
index cc32e2d1d..cc32e2d1d 100644
--- a/res/drawable-hdpi/action_settings.png
+++ b/core/src/main/res/drawable-hdpi/action_settings.png
Binary files differ
diff --git a/res/drawable-hdpi/action_settings_dark.png b/core/src/main/res/drawable-hdpi/action_settings_dark.png
index 3e4580e05..3e4580e05 100755
--- a/res/drawable-hdpi/action_settings_dark.png
+++ b/core/src/main/res/drawable-hdpi/action_settings_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/action_stream.png b/core/src/main/res/drawable-hdpi/action_stream.png
index 8fc7a7b1e..8fc7a7b1e 100644
--- a/res/drawable-hdpi/action_stream.png
+++ b/core/src/main/res/drawable-hdpi/action_stream.png
Binary files differ
diff --git a/res/drawable-hdpi/action_stream_dark.png b/core/src/main/res/drawable-hdpi/action_stream_dark.png
index 97b752cea..97b752cea 100644
--- a/res/drawable-hdpi/action_stream_dark.png
+++ b/core/src/main/res/drawable-hdpi/action_stream_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/av_download.png b/core/src/main/res/drawable-hdpi/av_download.png
index 5bceafb1e..5bceafb1e 100644
--- a/res/drawable-hdpi/av_download.png
+++ b/core/src/main/res/drawable-hdpi/av_download.png
Binary files differ
diff --git a/res/drawable-hdpi/av_download_dark.png b/core/src/main/res/drawable-hdpi/av_download_dark.png
index d5bfa457c..d5bfa457c 100755
--- a/res/drawable-hdpi/av_download_dark.png
+++ b/core/src/main/res/drawable-hdpi/av_download_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/av_fast_forward.png b/core/src/main/res/drawable-hdpi/av_fast_forward.png
index 58ee5c26c..58ee5c26c 100644
--- a/res/drawable-hdpi/av_fast_forward.png
+++ b/core/src/main/res/drawable-hdpi/av_fast_forward.png
Binary files differ
diff --git a/res/drawable-hdpi/av_fast_forward_dark.png b/core/src/main/res/drawable-hdpi/av_fast_forward_dark.png
index 237c4f846..237c4f846 100755
--- a/res/drawable-hdpi/av_fast_forward_dark.png
+++ b/core/src/main/res/drawable-hdpi/av_fast_forward_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/av_pause.png b/core/src/main/res/drawable-hdpi/av_pause.png
index 9661cfbb0..9661cfbb0 100644
--- a/res/drawable-hdpi/av_pause.png
+++ b/core/src/main/res/drawable-hdpi/av_pause.png
Binary files differ
diff --git a/res/drawable-hdpi/av_pause_dark.png b/core/src/main/res/drawable-hdpi/av_pause_dark.png
index 6b435bb0f..6b435bb0f 100755
--- a/res/drawable-hdpi/av_pause_dark.png
+++ b/core/src/main/res/drawable-hdpi/av_pause_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/av_play.png b/core/src/main/res/drawable-hdpi/av_play.png
index e70f0413e..e70f0413e 100644
--- a/res/drawable-hdpi/av_play.png
+++ b/core/src/main/res/drawable-hdpi/av_play.png
Binary files differ
diff --git a/res/drawable-hdpi/av_play_dark.png b/core/src/main/res/drawable-hdpi/av_play_dark.png
index df8a2ca28..df8a2ca28 100755
--- a/res/drawable-hdpi/av_play_dark.png
+++ b/core/src/main/res/drawable-hdpi/av_play_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/av_rewind.png b/core/src/main/res/drawable-hdpi/av_rewind.png
index e2f843ce2..e2f843ce2 100644
--- a/res/drawable-hdpi/av_rewind.png
+++ b/core/src/main/res/drawable-hdpi/av_rewind.png
Binary files differ
diff --git a/res/drawable-hdpi/av_rewind_dark.png b/core/src/main/res/drawable-hdpi/av_rewind_dark.png
index caf517498..caf517498 100755
--- a/res/drawable-hdpi/av_rewind_dark.png
+++ b/core/src/main/res/drawable-hdpi/av_rewind_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/content_discard.png b/core/src/main/res/drawable-hdpi/content_discard.png
index e9ce89e04..e9ce89e04 100644
--- a/res/drawable-hdpi/content_discard.png
+++ b/core/src/main/res/drawable-hdpi/content_discard.png
Binary files differ
diff --git a/res/drawable-hdpi/content_discard_dark.png b/core/src/main/res/drawable-hdpi/content_discard_dark.png
index ffd19d9e8..ffd19d9e8 100755
--- a/res/drawable-hdpi/content_discard_dark.png
+++ b/core/src/main/res/drawable-hdpi/content_discard_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/content_new.png b/core/src/main/res/drawable-hdpi/content_new.png
index 5741995cb..5741995cb 100644
--- a/res/drawable-hdpi/content_new.png
+++ b/core/src/main/res/drawable-hdpi/content_new.png
Binary files differ
diff --git a/res/drawable-hdpi/content_new_dark.png b/core/src/main/res/drawable-hdpi/content_new_dark.png
index ad8ada6bd..ad8ada6bd 100755
--- a/res/drawable-hdpi/content_new_dark.png
+++ b/core/src/main/res/drawable-hdpi/content_new_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/default_cover.png b/core/src/main/res/drawable-hdpi/default_cover.png
index a6e67e2ca..a6e67e2ca 100644
--- a/res/drawable-hdpi/default_cover.png
+++ b/core/src/main/res/drawable-hdpi/default_cover.png
Binary files differ
diff --git a/res/drawable-hdpi/default_cover_dark.png b/core/src/main/res/drawable-hdpi/default_cover_dark.png
index 0f650ee25..0f650ee25 100755
--- a/res/drawable-hdpi/default_cover_dark.png
+++ b/core/src/main/res/drawable-hdpi/default_cover_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/device_access_time.png b/core/src/main/res/drawable-hdpi/device_access_time.png
index 001549f38..001549f38 100644
--- a/res/drawable-hdpi/device_access_time.png
+++ b/core/src/main/res/drawable-hdpi/device_access_time.png
Binary files differ
diff --git a/res/drawable-hdpi/device_access_time_dark.png b/core/src/main/res/drawable-hdpi/device_access_time_dark.png
index 314ec9319..314ec9319 100755
--- a/res/drawable-hdpi/device_access_time_dark.png
+++ b/core/src/main/res/drawable-hdpi/device_access_time_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_action_overflow.png b/core/src/main/res/drawable-hdpi/ic_action_overflow.png
index 002fc4bfb..002fc4bfb 100644
--- a/res/drawable-hdpi/ic_action_overflow.png
+++ b/core/src/main/res/drawable-hdpi/ic_action_overflow.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-hdpi/ic_action_overflow_dark.png
index c8792cbe2..c8792cbe2 100644
--- a/res/drawable-hdpi/ic_action_overflow_dark.png
+++ b/core/src/main/res/drawable-hdpi/ic_action_overflow_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png
index 64b07728f..64b07728f 100755
--- a/res/drawable-hdpi/ic_action_pause_over_video.png
+++ b/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png
index a364ca7c2..a364ca7c2 100755
--- a/res/drawable-hdpi/ic_action_play_over_video.png
+++ b/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_drag_handle.png b/core/src/main/res/drawable-hdpi/ic_drag_handle.png
index 38ec201de..38ec201de 100755
--- a/res/drawable-hdpi/ic_drag_handle.png
+++ b/core/src/main/res/drawable-hdpi/ic_drag_handle.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png
index e96d23252..e96d23252 100755
--- a/res/drawable-hdpi/ic_drag_handle_dark.png
+++ b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_drawer.png b/core/src/main/res/drawable-hdpi/ic_drawer.png
index c59f601ca..c59f601ca 100644
--- a/res/drawable-hdpi/ic_drawer.png
+++ b/core/src/main/res/drawable-hdpi/ic_drawer.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_drawer_dark.png b/core/src/main/res/drawable-hdpi/ic_drawer_dark.png
index 6614ea4f4..6614ea4f4 100644
--- a/res/drawable-hdpi/ic_drawer_dark.png
+++ b/core/src/main/res/drawable-hdpi/ic_drawer_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_launcher.png b/core/src/main/res/drawable-hdpi/ic_launcher.png
index 994b763cc..994b763cc 100644
--- a/res/drawable-hdpi/ic_launcher.png
+++ b/core/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_new.png b/core/src/main/res/drawable-hdpi/ic_new.png
index 8ff519052..8ff519052 100755
--- a/res/drawable-hdpi/ic_new.png
+++ b/core/src/main/res/drawable-hdpi/ic_new.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_new_dark.png b/core/src/main/res/drawable-hdpi/ic_new_dark.png
index c8581e01c..c8581e01c 100755
--- a/res/drawable-hdpi/ic_new_dark.png
+++ b/core/src/main/res/drawable-hdpi/ic_new_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_stat_antenna.png b/core/src/main/res/drawable-hdpi/ic_stat_antenna_default.png
index 36d502492..36d502492 100644
--- a/res/drawable-hdpi/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-hdpi/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_stat_authentication.png b/core/src/main/res/drawable-hdpi/ic_stat_authentication.png
index c6b5efd33..c6b5efd33 100755
--- a/res/drawable-hdpi/ic_stat_authentication.png
+++ b/core/src/main/res/drawable-hdpi/ic_stat_authentication.png
Binary files differ
diff --git a/res/drawable-hdpi/location_web_site.png b/core/src/main/res/drawable-hdpi/location_web_site.png
index 6a2bc8857..6a2bc8857 100644
--- a/res/drawable-hdpi/location_web_site.png
+++ b/core/src/main/res/drawable-hdpi/location_web_site.png
Binary files differ
diff --git a/res/drawable-hdpi/location_web_site_dark.png b/core/src/main/res/drawable-hdpi/location_web_site_dark.png
index e154afdbc..e154afdbc 100755
--- a/res/drawable-hdpi/location_web_site_dark.png
+++ b/core/src/main/res/drawable-hdpi/location_web_site_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_accept.png b/core/src/main/res/drawable-hdpi/navigation_accept.png
index 58bf97217..58bf97217 100644
--- a/res/drawable-hdpi/navigation_accept.png
+++ b/core/src/main/res/drawable-hdpi/navigation_accept.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_accept_dark.png b/core/src/main/res/drawable-hdpi/navigation_accept_dark.png
index 53cf6877e..53cf6877e 100755
--- a/res/drawable-hdpi/navigation_accept_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_accept_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_cancel.png b/core/src/main/res/drawable-hdpi/navigation_cancel.png
index cde36e1fa..cde36e1fa 100644
--- a/res/drawable-hdpi/navigation_cancel.png
+++ b/core/src/main/res/drawable-hdpi/navigation_cancel.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_cancel_dark.png b/core/src/main/res/drawable-hdpi/navigation_cancel_dark.png
index 094eea589..094eea589 100755
--- a/res/drawable-hdpi/navigation_cancel_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_cancel_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_chapters.png b/core/src/main/res/drawable-hdpi/navigation_chapters.png
index b034459bc..b034459bc 100755
--- a/res/drawable-hdpi/navigation_chapters.png
+++ b/core/src/main/res/drawable-hdpi/navigation_chapters.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_chapters_dark.png b/core/src/main/res/drawable-hdpi/navigation_chapters_dark.png
index 7b0d4889c..7b0d4889c 100755
--- a/res/drawable-hdpi/navigation_chapters_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_chapters_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_collapse.png b/core/src/main/res/drawable-hdpi/navigation_collapse.png
index bd405bada..bd405bada 100755
--- a/res/drawable-hdpi/navigation_collapse.png
+++ b/core/src/main/res/drawable-hdpi/navigation_collapse.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_collapse_dark.png b/core/src/main/res/drawable-hdpi/navigation_collapse_dark.png
index ca78f2ec0..ca78f2ec0 100755
--- a/res/drawable-hdpi/navigation_collapse_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_collapse_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_expand.png b/core/src/main/res/drawable-hdpi/navigation_expand.png
index 8225e74b7..8225e74b7 100644
--- a/res/drawable-hdpi/navigation_expand.png
+++ b/core/src/main/res/drawable-hdpi/navigation_expand.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_expand_dark.png b/core/src/main/res/drawable-hdpi/navigation_expand_dark.png
index 1676b104b..1676b104b 100755
--- a/res/drawable-hdpi/navigation_expand_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_expand_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_refresh.png b/core/src/main/res/drawable-hdpi/navigation_refresh.png
index 479aca465..479aca465 100644
--- a/res/drawable-hdpi/navigation_refresh.png
+++ b/core/src/main/res/drawable-hdpi/navigation_refresh.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_refresh_dark.png b/core/src/main/res/drawable-hdpi/navigation_refresh_dark.png
index bb9d855f7..bb9d855f7 100755
--- a/res/drawable-hdpi/navigation_refresh_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_refresh_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_shownotes.png b/core/src/main/res/drawable-hdpi/navigation_shownotes.png
index c5f6c97b2..c5f6c97b2 100755
--- a/res/drawable-hdpi/navigation_shownotes.png
+++ b/core/src/main/res/drawable-hdpi/navigation_shownotes.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_shownotes_dark.png b/core/src/main/res/drawable-hdpi/navigation_shownotes_dark.png
index e45ea1fd9..e45ea1fd9 100755
--- a/res/drawable-hdpi/navigation_shownotes_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_shownotes_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_up.png b/core/src/main/res/drawable-hdpi/navigation_up.png
index a2cf2ba52..a2cf2ba52 100755
--- a/res/drawable-hdpi/navigation_up.png
+++ b/core/src/main/res/drawable-hdpi/navigation_up.png
Binary files differ
diff --git a/res/drawable-hdpi/navigation_up_dark.png b/core/src/main/res/drawable-hdpi/navigation_up_dark.png
index f2374a323..f2374a323 100755
--- a/res/drawable-hdpi/navigation_up_dark.png
+++ b/core/src/main/res/drawable-hdpi/navigation_up_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/social_share.png b/core/src/main/res/drawable-hdpi/social_share.png
index 47ae18674..47ae18674 100644
--- a/res/drawable-hdpi/social_share.png
+++ b/core/src/main/res/drawable-hdpi/social_share.png
Binary files differ
diff --git a/res/drawable-hdpi/social_share_dark.png b/core/src/main/res/drawable-hdpi/social_share_dark.png
index c329f58da..c329f58da 100755
--- a/res/drawable-hdpi/social_share_dark.png
+++ b/core/src/main/res/drawable-hdpi/social_share_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/spinner_button.9.png b/core/src/main/res/drawable-hdpi/spinner_button.9.png
index fa68a137f..fa68a137f 100644
--- a/res/drawable-hdpi/spinner_button.9.png
+++ b/core/src/main/res/drawable-hdpi/spinner_button.9.png
Binary files differ
diff --git a/res/drawable-hdpi/spinner_button_dark.9.png b/core/src/main/res/drawable-hdpi/spinner_button_dark.9.png
index 88f8765cd..88f8765cd 100644
--- a/res/drawable-hdpi/spinner_button_dark.9.png
+++ b/core/src/main/res/drawable-hdpi/spinner_button_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/stat_notify_sync.png b/core/src/main/res/drawable-hdpi/stat_notify_sync.png
index bfb8110fe..bfb8110fe 100644
--- a/res/drawable-hdpi/stat_notify_sync.png
+++ b/core/src/main/res/drawable-hdpi/stat_notify_sync.png
Binary files differ
diff --git a/res/drawable-hdpi/stat_notify_sync_error.png b/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png
index b340a313e..b340a313e 100644
--- a/res/drawable-hdpi/stat_notify_sync_error.png
+++ b/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png
Binary files differ
diff --git a/res/drawable-hdpi/stat_playlist.png b/core/src/main/res/drawable-hdpi/stat_playlist.png
index 93c3f02b8..93c3f02b8 100644
--- a/res/drawable-hdpi/stat_playlist.png
+++ b/core/src/main/res/drawable-hdpi/stat_playlist.png
Binary files differ
diff --git a/res/drawable-hdpi/stat_playlist_dark.png b/core/src/main/res/drawable-hdpi/stat_playlist_dark.png
index 972ce98b3..972ce98b3 100644
--- a/res/drawable-hdpi/stat_playlist_dark.png
+++ b/core/src/main/res/drawable-hdpi/stat_playlist_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/type_audio.png b/core/src/main/res/drawable-hdpi/type_audio.png
index d43e8a33c..d43e8a33c 100644
--- a/res/drawable-hdpi/type_audio.png
+++ b/core/src/main/res/drawable-hdpi/type_audio.png
Binary files differ
diff --git a/res/drawable-hdpi/type_audio_dark.png b/core/src/main/res/drawable-hdpi/type_audio_dark.png
index 7b69ea56b..7b69ea56b 100755
--- a/res/drawable-hdpi/type_audio_dark.png
+++ b/core/src/main/res/drawable-hdpi/type_audio_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/type_video.png b/core/src/main/res/drawable-hdpi/type_video.png
index f9467146c..f9467146c 100644
--- a/res/drawable-hdpi/type_video.png
+++ b/core/src/main/res/drawable-hdpi/type_video.png
Binary files differ
diff --git a/res/drawable-hdpi/type_video_dark.png b/core/src/main/res/drawable-hdpi/type_video_dark.png
index 37f3a93a2..37f3a93a2 100755
--- a/res/drawable-hdpi/type_video_dark.png
+++ b/core/src/main/res/drawable-hdpi/type_video_dark.png
Binary files differ
diff --git a/res/drawable-ldpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna_default.png
index e44f42510..e44f42510 100644
--- a/res/drawable-ldpi-v11/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-ldpi/action_stream.png b/core/src/main/res/drawable-ldpi/action_stream.png
index 5ae4f3d34..5ae4f3d34 100644
--- a/res/drawable-ldpi/action_stream.png
+++ b/core/src/main/res/drawable-ldpi/action_stream.png
Binary files differ
diff --git a/res/drawable-ldpi/action_stream_dark.png b/core/src/main/res/drawable-ldpi/action_stream_dark.png
index f3c81fff8..f3c81fff8 100644
--- a/res/drawable-ldpi/action_stream_dark.png
+++ b/core/src/main/res/drawable-ldpi/action_stream_dark.png
Binary files differ
diff --git a/res/drawable-ldpi/ic_launcher.png b/core/src/main/res/drawable-ldpi/ic_launcher.png
index 546090dd2..546090dd2 100644
--- a/res/drawable-ldpi/ic_launcher.png
+++ b/core/src/main/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-ldpi/ic_stat_antenna.png b/core/src/main/res/drawable-ldpi/ic_stat_antenna_default.png
index 63d72970d..63d72970d 100644
--- a/res/drawable-ldpi/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-ldpi/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-ldpi/stat_playlist.png b/core/src/main/res/drawable-ldpi/stat_playlist.png
index 3a702ef2f..3a702ef2f 100644
--- a/res/drawable-ldpi/stat_playlist.png
+++ b/core/src/main/res/drawable-ldpi/stat_playlist.png
Binary files differ
diff --git a/res/drawable-ldpi/stat_playlist_dark.png b/core/src/main/res/drawable-ldpi/stat_playlist_dark.png
index b82b06f67..b82b06f67 100644
--- a/res/drawable-ldpi/stat_playlist_dark.png
+++ b/core/src/main/res/drawable-ldpi/stat_playlist_dark.png
Binary files differ
diff --git a/res/drawable-mdpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna_default.png
index 8808dedc7..8808dedc7 100644
--- a/res/drawable-mdpi-v11/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-mdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png
index de69b17c0..de69b17c0 100755
--- a/res/drawable-mdpi-v11/ic_stat_authentication.png
+++ b/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png
Binary files differ
diff --git a/res/drawable-mdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png
index 1be8677f1..1be8677f1 100644
--- a/res/drawable-mdpi-v11/stat_notify_sync.png
+++ b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png
Binary files differ
diff --git a/res/drawable-mdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png
index 30658c583..30658c583 100644
--- a/res/drawable-mdpi-v11/stat_notify_sync_error.png
+++ b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png
Binary files differ
diff --git a/res/drawable-mdpi/action_about.png b/core/src/main/res/drawable-mdpi/action_about.png
index 7c57436fc..7c57436fc 100644
--- a/res/drawable-mdpi/action_about.png
+++ b/core/src/main/res/drawable-mdpi/action_about.png
Binary files differ
diff --git a/res/drawable-mdpi/action_about_dark.png b/core/src/main/res/drawable-mdpi/action_about_dark.png
index d7b7e6986..d7b7e6986 100755
--- a/res/drawable-mdpi/action_about_dark.png
+++ b/core/src/main/res/drawable-mdpi/action_about_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/action_search.png b/core/src/main/res/drawable-mdpi/action_search.png
index 3aa644048..3aa644048 100644
--- a/res/drawable-mdpi/action_search.png
+++ b/core/src/main/res/drawable-mdpi/action_search.png
Binary files differ
diff --git a/res/drawable-mdpi/action_search_dark.png b/core/src/main/res/drawable-mdpi/action_search_dark.png
index 587d9e0bf..587d9e0bf 100755
--- a/res/drawable-mdpi/action_search_dark.png
+++ b/core/src/main/res/drawable-mdpi/action_search_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/action_settings.png b/core/src/main/res/drawable-mdpi/action_settings.png
index dc66d914e..dc66d914e 100644
--- a/res/drawable-mdpi/action_settings.png
+++ b/core/src/main/res/drawable-mdpi/action_settings.png
Binary files differ
diff --git a/res/drawable-mdpi/action_settings_dark.png b/core/src/main/res/drawable-mdpi/action_settings_dark.png
index d3e42edcb..d3e42edcb 100755
--- a/res/drawable-mdpi/action_settings_dark.png
+++ b/core/src/main/res/drawable-mdpi/action_settings_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/action_stream.png b/core/src/main/res/drawable-mdpi/action_stream.png
index 4bc7d8379..4bc7d8379 100644
--- a/res/drawable-mdpi/action_stream.png
+++ b/core/src/main/res/drawable-mdpi/action_stream.png
Binary files differ
diff --git a/res/drawable-mdpi/action_stream_dark.png b/core/src/main/res/drawable-mdpi/action_stream_dark.png
index 1f4fdd186..1f4fdd186 100644
--- a/res/drawable-mdpi/action_stream_dark.png
+++ b/core/src/main/res/drawable-mdpi/action_stream_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/av_download.png b/core/src/main/res/drawable-mdpi/av_download.png
index 678ecfad4..678ecfad4 100644
--- a/res/drawable-mdpi/av_download.png
+++ b/core/src/main/res/drawable-mdpi/av_download.png
Binary files differ
diff --git a/res/drawable-mdpi/av_download_dark.png b/core/src/main/res/drawable-mdpi/av_download_dark.png
index cc4d9576b..cc4d9576b 100755
--- a/res/drawable-mdpi/av_download_dark.png
+++ b/core/src/main/res/drawable-mdpi/av_download_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/av_fast_forward.png b/core/src/main/res/drawable-mdpi/av_fast_forward.png
index 43f15a245..43f15a245 100644
--- a/res/drawable-mdpi/av_fast_forward.png
+++ b/core/src/main/res/drawable-mdpi/av_fast_forward.png
Binary files differ
diff --git a/res/drawable-mdpi/av_fast_forward_dark.png b/core/src/main/res/drawable-mdpi/av_fast_forward_dark.png
index fc8074cea..fc8074cea 100755
--- a/res/drawable-mdpi/av_fast_forward_dark.png
+++ b/core/src/main/res/drawable-mdpi/av_fast_forward_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/av_pause.png b/core/src/main/res/drawable-mdpi/av_pause.png
index 01858e34d..01858e34d 100644
--- a/res/drawable-mdpi/av_pause.png
+++ b/core/src/main/res/drawable-mdpi/av_pause.png
Binary files differ
diff --git a/res/drawable-mdpi/av_pause_dark.png b/core/src/main/res/drawable-mdpi/av_pause_dark.png
index a5aee6f2c..a5aee6f2c 100755
--- a/res/drawable-mdpi/av_pause_dark.png
+++ b/core/src/main/res/drawable-mdpi/av_pause_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/av_play.png b/core/src/main/res/drawable-mdpi/av_play.png
index 1e3bc97af..1e3bc97af 100644
--- a/res/drawable-mdpi/av_play.png
+++ b/core/src/main/res/drawable-mdpi/av_play.png
Binary files differ
diff --git a/res/drawable-mdpi/av_play_dark.png b/core/src/main/res/drawable-mdpi/av_play_dark.png
index 6a40cd5f7..6a40cd5f7 100755
--- a/res/drawable-mdpi/av_play_dark.png
+++ b/core/src/main/res/drawable-mdpi/av_play_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/av_rewind.png b/core/src/main/res/drawable-mdpi/av_rewind.png
index a2f7f5895..a2f7f5895 100644
--- a/res/drawable-mdpi/av_rewind.png
+++ b/core/src/main/res/drawable-mdpi/av_rewind.png
Binary files differ
diff --git a/res/drawable-mdpi/av_rewind_dark.png b/core/src/main/res/drawable-mdpi/av_rewind_dark.png
index e555a2046..e555a2046 100755
--- a/res/drawable-mdpi/av_rewind_dark.png
+++ b/core/src/main/res/drawable-mdpi/av_rewind_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/content_discard.png b/core/src/main/res/drawable-mdpi/content_discard.png
index cedb1085b..cedb1085b 100644
--- a/res/drawable-mdpi/content_discard.png
+++ b/core/src/main/res/drawable-mdpi/content_discard.png
Binary files differ
diff --git a/res/drawable-mdpi/content_discard_dark.png b/core/src/main/res/drawable-mdpi/content_discard_dark.png
index a8ee5f253..a8ee5f253 100755
--- a/res/drawable-mdpi/content_discard_dark.png
+++ b/core/src/main/res/drawable-mdpi/content_discard_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/content_new.png b/core/src/main/res/drawable-mdpi/content_new.png
index 884c9d270..884c9d270 100644
--- a/res/drawable-mdpi/content_new.png
+++ b/core/src/main/res/drawable-mdpi/content_new.png
Binary files differ
diff --git a/res/drawable-mdpi/content_new_dark.png b/core/src/main/res/drawable-mdpi/content_new_dark.png
index 4d5d484b3..4d5d484b3 100755
--- a/res/drawable-mdpi/content_new_dark.png
+++ b/core/src/main/res/drawable-mdpi/content_new_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/default_cover.png b/core/src/main/res/drawable-mdpi/default_cover.png
index 62adf32ab..62adf32ab 100644
--- a/res/drawable-mdpi/default_cover.png
+++ b/core/src/main/res/drawable-mdpi/default_cover.png
Binary files differ
diff --git a/res/drawable-mdpi/default_cover_dark.png b/core/src/main/res/drawable-mdpi/default_cover_dark.png
index d6235554b..d6235554b 100755
--- a/res/drawable-mdpi/default_cover_dark.png
+++ b/core/src/main/res/drawable-mdpi/default_cover_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/device_access_time.png b/core/src/main/res/drawable-mdpi/device_access_time.png
index de9b4fb2a..de9b4fb2a 100644
--- a/res/drawable-mdpi/device_access_time.png
+++ b/core/src/main/res/drawable-mdpi/device_access_time.png
Binary files differ
diff --git a/res/drawable-mdpi/device_access_time_dark.png b/core/src/main/res/drawable-mdpi/device_access_time_dark.png
index a09df2b99..a09df2b99 100755
--- a/res/drawable-mdpi/device_access_time_dark.png
+++ b/core/src/main/res/drawable-mdpi/device_access_time_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_overflow.png b/core/src/main/res/drawable-mdpi/ic_action_overflow.png
index 6f0fb23f4..6f0fb23f4 100644
--- a/res/drawable-mdpi/ic_action_overflow.png
+++ b/core/src/main/res/drawable-mdpi/ic_action_overflow.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-mdpi/ic_action_overflow_dark.png
index b4a4a221f..b4a4a221f 100644
--- a/res/drawable-mdpi/ic_action_overflow_dark.png
+++ b/core/src/main/res/drawable-mdpi/ic_action_overflow_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png
index f478ac321..f478ac321 100755
--- a/res/drawable-mdpi/ic_action_pause_over_video.png
+++ b/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png
index 835ff3636..835ff3636 100755
--- a/res/drawable-mdpi/ic_action_play_over_video.png
+++ b/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_drag_handle.png b/core/src/main/res/drawable-mdpi/ic_drag_handle.png
index 4afbdc67d..4afbdc67d 100755
--- a/res/drawable-mdpi/ic_drag_handle.png
+++ b/core/src/main/res/drawable-mdpi/ic_drag_handle.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png
index 2b25c4101..2b25c4101 100755
--- a/res/drawable-mdpi/ic_drag_handle_dark.png
+++ b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_drawer.png b/core/src/main/res/drawable-mdpi/ic_drawer.png
index 1ed2c56ee..1ed2c56ee 100644
--- a/res/drawable-mdpi/ic_drawer.png
+++ b/core/src/main/res/drawable-mdpi/ic_drawer.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_drawer_dark.png b/core/src/main/res/drawable-mdpi/ic_drawer_dark.png
index b05c026c1..b05c026c1 100644
--- a/res/drawable-mdpi/ic_drawer_dark.png
+++ b/core/src/main/res/drawable-mdpi/ic_drawer_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_launcher.png b/core/src/main/res/drawable-mdpi/ic_launcher.png
index 403dfabc4..403dfabc4 100644
--- a/res/drawable-mdpi/ic_launcher.png
+++ b/core/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_new.png b/core/src/main/res/drawable-mdpi/ic_new.png
index 84994bd10..84994bd10 100755
--- a/res/drawable-mdpi/ic_new.png
+++ b/core/src/main/res/drawable-mdpi/ic_new.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_new_dark.png b/core/src/main/res/drawable-mdpi/ic_new_dark.png
index b723618b4..b723618b4 100755
--- a/res/drawable-mdpi/ic_new_dark.png
+++ b/core/src/main/res/drawable-mdpi/ic_new_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_stat_antenna.png b/core/src/main/res/drawable-mdpi/ic_stat_antenna_default.png
index 8b1206b51..8b1206b51 100644
--- a/res/drawable-mdpi/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-mdpi/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_stat_authentication.png b/core/src/main/res/drawable-mdpi/ic_stat_authentication.png
index cadfb9643..cadfb9643 100755
--- a/res/drawable-mdpi/ic_stat_authentication.png
+++ b/core/src/main/res/drawable-mdpi/ic_stat_authentication.png
Binary files differ
diff --git a/res/drawable-mdpi/location_web_site.png b/core/src/main/res/drawable-mdpi/location_web_site.png
index f146cf997..f146cf997 100644
--- a/res/drawable-mdpi/location_web_site.png
+++ b/core/src/main/res/drawable-mdpi/location_web_site.png
Binary files differ
diff --git a/res/drawable-mdpi/location_web_site_dark.png b/core/src/main/res/drawable-mdpi/location_web_site_dark.png
index 41b56ec92..41b56ec92 100755
--- a/res/drawable-mdpi/location_web_site_dark.png
+++ b/core/src/main/res/drawable-mdpi/location_web_site_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_accept.png b/core/src/main/res/drawable-mdpi/navigation_accept.png
index cf5fab3ad..cf5fab3ad 100644
--- a/res/drawable-mdpi/navigation_accept.png
+++ b/core/src/main/res/drawable-mdpi/navigation_accept.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_accept_dark.png b/core/src/main/res/drawable-mdpi/navigation_accept_dark.png
index 35cda8e11..35cda8e11 100755
--- a/res/drawable-mdpi/navigation_accept_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_accept_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_cancel.png b/core/src/main/res/drawable-mdpi/navigation_cancel.png
index 9f4c3d6a2..9f4c3d6a2 100644
--- a/res/drawable-mdpi/navigation_cancel.png
+++ b/core/src/main/res/drawable-mdpi/navigation_cancel.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_cancel_dark.png b/core/src/main/res/drawable-mdpi/navigation_cancel_dark.png
index 3336760d5..3336760d5 100755
--- a/res/drawable-mdpi/navigation_cancel_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_cancel_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_chapters.png b/core/src/main/res/drawable-mdpi/navigation_chapters.png
index b1884726c..b1884726c 100755
--- a/res/drawable-mdpi/navigation_chapters.png
+++ b/core/src/main/res/drawable-mdpi/navigation_chapters.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_chapters_dark.png b/core/src/main/res/drawable-mdpi/navigation_chapters_dark.png
index 1042294e4..1042294e4 100755
--- a/res/drawable-mdpi/navigation_chapters_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_chapters_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_collapse.png b/core/src/main/res/drawable-mdpi/navigation_collapse.png
index 6673c7aea..6673c7aea 100755
--- a/res/drawable-mdpi/navigation_collapse.png
+++ b/core/src/main/res/drawable-mdpi/navigation_collapse.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_collapse_dark.png b/core/src/main/res/drawable-mdpi/navigation_collapse_dark.png
index 01d6511ee..01d6511ee 100755
--- a/res/drawable-mdpi/navigation_collapse_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_collapse_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_expand.png b/core/src/main/res/drawable-mdpi/navigation_expand.png
index 78107862c..78107862c 100644
--- a/res/drawable-mdpi/navigation_expand.png
+++ b/core/src/main/res/drawable-mdpi/navigation_expand.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_expand_dark.png b/core/src/main/res/drawable-mdpi/navigation_expand_dark.png
index aa2b40ca0..aa2b40ca0 100755
--- a/res/drawable-mdpi/navigation_expand_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_expand_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_refresh.png b/core/src/main/res/drawable-mdpi/navigation_refresh.png
index 63e70e178..63e70e178 100644
--- a/res/drawable-mdpi/navigation_refresh.png
+++ b/core/src/main/res/drawable-mdpi/navigation_refresh.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_refresh_dark.png b/core/src/main/res/drawable-mdpi/navigation_refresh_dark.png
index bd611e8e2..bd611e8e2 100755
--- a/res/drawable-mdpi/navigation_refresh_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_refresh_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_shownotes.png b/core/src/main/res/drawable-mdpi/navigation_shownotes.png
index ec6a2bf8f..ec6a2bf8f 100755
--- a/res/drawable-mdpi/navigation_shownotes.png
+++ b/core/src/main/res/drawable-mdpi/navigation_shownotes.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_shownotes_dark.png b/core/src/main/res/drawable-mdpi/navigation_shownotes_dark.png
index 9c748b0b5..9c748b0b5 100755
--- a/res/drawable-mdpi/navigation_shownotes_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_shownotes_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_up.png b/core/src/main/res/drawable-mdpi/navigation_up.png
index 1ee248a79..1ee248a79 100755
--- a/res/drawable-mdpi/navigation_up.png
+++ b/core/src/main/res/drawable-mdpi/navigation_up.png
Binary files differ
diff --git a/res/drawable-mdpi/navigation_up_dark.png b/core/src/main/res/drawable-mdpi/navigation_up_dark.png
index 8ef44cbac..8ef44cbac 100755
--- a/res/drawable-mdpi/navigation_up_dark.png
+++ b/core/src/main/res/drawable-mdpi/navigation_up_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/social_share.png b/core/src/main/res/drawable-mdpi/social_share.png
index 8aa52bc7d..8aa52bc7d 100644
--- a/res/drawable-mdpi/social_share.png
+++ b/core/src/main/res/drawable-mdpi/social_share.png
Binary files differ
diff --git a/res/drawable-mdpi/social_share_dark.png b/core/src/main/res/drawable-mdpi/social_share_dark.png
index 056deb57b..056deb57b 100755
--- a/res/drawable-mdpi/social_share_dark.png
+++ b/core/src/main/res/drawable-mdpi/social_share_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/spinner_button.9.png b/core/src/main/res/drawable-mdpi/spinner_button.9.png
index 716560bb1..716560bb1 100644
--- a/res/drawable-mdpi/spinner_button.9.png
+++ b/core/src/main/res/drawable-mdpi/spinner_button.9.png
Binary files differ
diff --git a/res/drawable-mdpi/spinner_button_dark.9.png b/core/src/main/res/drawable-mdpi/spinner_button_dark.9.png
index 8d7594685..8d7594685 100644
--- a/res/drawable-mdpi/spinner_button_dark.9.png
+++ b/core/src/main/res/drawable-mdpi/spinner_button_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_notify_sync.png b/core/src/main/res/drawable-mdpi/stat_notify_sync.png
index 03ce57a47..03ce57a47 100644
--- a/res/drawable-mdpi/stat_notify_sync.png
+++ b/core/src/main/res/drawable-mdpi/stat_notify_sync.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_notify_sync_error.png b/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png
index f849b5040..f849b5040 100644
--- a/res/drawable-mdpi/stat_notify_sync_error.png
+++ b/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_playlist.png b/core/src/main/res/drawable-mdpi/stat_playlist.png
index 136a7a265..136a7a265 100644
--- a/res/drawable-mdpi/stat_playlist.png
+++ b/core/src/main/res/drawable-mdpi/stat_playlist.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_playlist_dark.png b/core/src/main/res/drawable-mdpi/stat_playlist_dark.png
index 7ed94b13c..7ed94b13c 100644
--- a/res/drawable-mdpi/stat_playlist_dark.png
+++ b/core/src/main/res/drawable-mdpi/stat_playlist_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/type_audio.png b/core/src/main/res/drawable-mdpi/type_audio.png
index 4ec9efd97..4ec9efd97 100644
--- a/res/drawable-mdpi/type_audio.png
+++ b/core/src/main/res/drawable-mdpi/type_audio.png
Binary files differ
diff --git a/res/drawable-mdpi/type_audio_dark.png b/core/src/main/res/drawable-mdpi/type_audio_dark.png
index f8dd8469c..f8dd8469c 100755
--- a/res/drawable-mdpi/type_audio_dark.png
+++ b/core/src/main/res/drawable-mdpi/type_audio_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/type_video.png b/core/src/main/res/drawable-mdpi/type_video.png
index a2722b812..a2722b812 100644
--- a/res/drawable-mdpi/type_video.png
+++ b/core/src/main/res/drawable-mdpi/type_video.png
Binary files differ
diff --git a/res/drawable-mdpi/type_video_dark.png b/core/src/main/res/drawable-mdpi/type_video_dark.png
index aa0c320dc..aa0c320dc 100755
--- a/res/drawable-mdpi/type_video_dark.png
+++ b/core/src/main/res/drawable-mdpi/type_video_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna_default.png
index 59de64c87..59de64c87 100644
--- a/res/drawable-xhdpi-v11/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-xhdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png
index f58fb21df..f58fb21df 100755
--- a/res/drawable-xhdpi-v11/ic_stat_authentication.png
+++ b/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png
Binary files differ
diff --git a/res/drawable-xhdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png
index b3bf21ffe..b3bf21ffe 100644
--- a/res/drawable-xhdpi-v11/stat_notify_sync.png
+++ b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png
Binary files differ
diff --git a/res/drawable-xhdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png
index 33582ef10..33582ef10 100644
--- a/res/drawable-xhdpi-v11/stat_notify_sync_error.png
+++ b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_about.png b/core/src/main/res/drawable-xhdpi/action_about.png
index 2641f142a..2641f142a 100644
--- a/res/drawable-xhdpi/action_about.png
+++ b/core/src/main/res/drawable-xhdpi/action_about.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_about_dark.png b/core/src/main/res/drawable-xhdpi/action_about_dark.png
index 4ee903f07..4ee903f07 100755
--- a/res/drawable-xhdpi/action_about_dark.png
+++ b/core/src/main/res/drawable-xhdpi/action_about_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_search.png b/core/src/main/res/drawable-xhdpi/action_search.png
index 804420aee..804420aee 100644
--- a/res/drawable-xhdpi/action_search.png
+++ b/core/src/main/res/drawable-xhdpi/action_search.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_search_dark.png b/core/src/main/res/drawable-xhdpi/action_search_dark.png
index 3549f84dd..3549f84dd 100755
--- a/res/drawable-xhdpi/action_search_dark.png
+++ b/core/src/main/res/drawable-xhdpi/action_search_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_settings.png b/core/src/main/res/drawable-xhdpi/action_settings.png
index 04b65dc34..04b65dc34 100644
--- a/res/drawable-xhdpi/action_settings.png
+++ b/core/src/main/res/drawable-xhdpi/action_settings.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_settings_dark.png b/core/src/main/res/drawable-xhdpi/action_settings_dark.png
index 09b014834..09b014834 100755
--- a/res/drawable-xhdpi/action_settings_dark.png
+++ b/core/src/main/res/drawable-xhdpi/action_settings_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_stream.png b/core/src/main/res/drawable-xhdpi/action_stream.png
index f87f2da5e..f87f2da5e 100644
--- a/res/drawable-xhdpi/action_stream.png
+++ b/core/src/main/res/drawable-xhdpi/action_stream.png
Binary files differ
diff --git a/res/drawable-xhdpi/action_stream_dark.png b/core/src/main/res/drawable-xhdpi/action_stream_dark.png
index d3721318c..d3721318c 100644
--- a/res/drawable-xhdpi/action_stream_dark.png
+++ b/core/src/main/res/drawable-xhdpi/action_stream_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_download.png b/core/src/main/res/drawable-xhdpi/av_download.png
index dfe81e064..dfe81e064 100644
--- a/res/drawable-xhdpi/av_download.png
+++ b/core/src/main/res/drawable-xhdpi/av_download.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_download_dark.png b/core/src/main/res/drawable-xhdpi/av_download_dark.png
index bc0ced50f..bc0ced50f 100755
--- a/res/drawable-xhdpi/av_download_dark.png
+++ b/core/src/main/res/drawable-xhdpi/av_download_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_fast_forward.png b/core/src/main/res/drawable-xhdpi/av_fast_forward.png
index 026c3b779..026c3b779 100644
--- a/res/drawable-xhdpi/av_fast_forward.png
+++ b/core/src/main/res/drawable-xhdpi/av_fast_forward.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_fast_forward_dark.png b/core/src/main/res/drawable-xhdpi/av_fast_forward_dark.png
index 896334d47..896334d47 100755
--- a/res/drawable-xhdpi/av_fast_forward_dark.png
+++ b/core/src/main/res/drawable-xhdpi/av_fast_forward_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_pause.png b/core/src/main/res/drawable-xhdpi/av_pause.png
index 97d6f91ac..97d6f91ac 100644
--- a/res/drawable-xhdpi/av_pause.png
+++ b/core/src/main/res/drawable-xhdpi/av_pause.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_pause_dark.png b/core/src/main/res/drawable-xhdpi/av_pause_dark.png
index 333c1b24d..333c1b24d 100755
--- a/res/drawable-xhdpi/av_pause_dark.png
+++ b/core/src/main/res/drawable-xhdpi/av_pause_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_play.png b/core/src/main/res/drawable-xhdpi/av_play.png
index 2d67d31e7..2d67d31e7 100644
--- a/res/drawable-xhdpi/av_play.png
+++ b/core/src/main/res/drawable-xhdpi/av_play.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_play_dark.png b/core/src/main/res/drawable-xhdpi/av_play_dark.png
index 51124993d..51124993d 100755
--- a/res/drawable-xhdpi/av_play_dark.png
+++ b/core/src/main/res/drawable-xhdpi/av_play_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_rewind.png b/core/src/main/res/drawable-xhdpi/av_rewind.png
index 57b41744d..57b41744d 100644
--- a/res/drawable-xhdpi/av_rewind.png
+++ b/core/src/main/res/drawable-xhdpi/av_rewind.png
Binary files differ
diff --git a/res/drawable-xhdpi/av_rewind_dark.png b/core/src/main/res/drawable-xhdpi/av_rewind_dark.png
index 69dda127c..69dda127c 100755
--- a/res/drawable-xhdpi/av_rewind_dark.png
+++ b/core/src/main/res/drawable-xhdpi/av_rewind_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/content_discard.png b/core/src/main/res/drawable-xhdpi/content_discard.png
index 98c73da1f..98c73da1f 100644
--- a/res/drawable-xhdpi/content_discard.png
+++ b/core/src/main/res/drawable-xhdpi/content_discard.png
Binary files differ
diff --git a/res/drawable-xhdpi/content_discard_dark.png b/core/src/main/res/drawable-xhdpi/content_discard_dark.png
index 412b33354..412b33354 100755
--- a/res/drawable-xhdpi/content_discard_dark.png
+++ b/core/src/main/res/drawable-xhdpi/content_discard_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/content_new.png b/core/src/main/res/drawable-xhdpi/content_new.png
index 9b48a63da..9b48a63da 100644
--- a/res/drawable-xhdpi/content_new.png
+++ b/core/src/main/res/drawable-xhdpi/content_new.png
Binary files differ
diff --git a/res/drawable-xhdpi/content_new_dark.png b/core/src/main/res/drawable-xhdpi/content_new_dark.png
index 23b9a1c18..23b9a1c18 100755
--- a/res/drawable-xhdpi/content_new_dark.png
+++ b/core/src/main/res/drawable-xhdpi/content_new_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/content_remove.png b/core/src/main/res/drawable-xhdpi/content_remove.png
index ca7d159fd..ca7d159fd 100644
--- a/res/drawable-xhdpi/content_remove.png
+++ b/core/src/main/res/drawable-xhdpi/content_remove.png
Binary files differ
diff --git a/res/drawable-xhdpi/content_remove_dark.png b/core/src/main/res/drawable-xhdpi/content_remove_dark.png
index f391760ef..f391760ef 100755
--- a/res/drawable-xhdpi/content_remove_dark.png
+++ b/core/src/main/res/drawable-xhdpi/content_remove_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/default_cover.png b/core/src/main/res/drawable-xhdpi/default_cover.png
index c2f4578f9..c2f4578f9 100644
--- a/res/drawable-xhdpi/default_cover.png
+++ b/core/src/main/res/drawable-xhdpi/default_cover.png
Binary files differ
diff --git a/res/drawable-xhdpi/default_cover_dark.png b/core/src/main/res/drawable-xhdpi/default_cover_dark.png
index 3f93e4f65..3f93e4f65 100755
--- a/res/drawable-xhdpi/default_cover_dark.png
+++ b/core/src/main/res/drawable-xhdpi/default_cover_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/device_access_time.png b/core/src/main/res/drawable-xhdpi/device_access_time.png
index 2beae08c3..2beae08c3 100644
--- a/res/drawable-xhdpi/device_access_time.png
+++ b/core/src/main/res/drawable-xhdpi/device_access_time.png
Binary files differ
diff --git a/res/drawable-xhdpi/device_access_time_dark.png b/core/src/main/res/drawable-xhdpi/device_access_time_dark.png
index c8771db97..c8771db97 100755
--- a/res/drawable-xhdpi/device_access_time_dark.png
+++ b/core/src/main/res/drawable-xhdpi/device_access_time_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_action_overflow.png b/core/src/main/res/drawable-xhdpi/ic_action_overflow.png
index 7ba4e10ea..7ba4e10ea 100644
--- a/res/drawable-xhdpi/ic_action_overflow.png
+++ b/core/src/main/res/drawable-xhdpi/ic_action_overflow.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png
index 5d8af5d63..5d8af5d63 100644
--- a/res/drawable-xhdpi/ic_action_overflow_dark.png
+++ b/core/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png
index b0777a023..b0777a023 100755
--- a/res/drawable-xhdpi/ic_action_pause_over_video.png
+++ b/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png
index 24331a48c..24331a48c 100755
--- a/res/drawable-xhdpi/ic_action_play_over_video.png
+++ b/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle.png
index 5bdcac342..5bdcac342 100755
--- a/res/drawable-xhdpi/ic_drag_handle.png
+++ b/core/src/main/res/drawable-xhdpi/ic_drag_handle.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png
index d341c7c82..d341c7c82 100755
--- a/res/drawable-xhdpi/ic_drag_handle_dark.png
+++ b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drawer.png b/core/src/main/res/drawable-xhdpi/ic_drawer.png
index a5fa74def..a5fa74def 100644
--- a/res/drawable-xhdpi/ic_drawer.png
+++ b/core/src/main/res/drawable-xhdpi/ic_drawer.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drawer_dark.png b/core/src/main/res/drawable-xhdpi/ic_drawer_dark.png
index bcf49dd73..bcf49dd73 100644
--- a/res/drawable-xhdpi/ic_drawer_dark.png
+++ b/core/src/main/res/drawable-xhdpi/ic_drawer_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_launcher.png b/core/src/main/res/drawable-xhdpi/ic_launcher.png
index 857a1b12e..857a1b12e 100644
--- a/res/drawable-xhdpi/ic_launcher.png
+++ b/core/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_new.png b/core/src/main/res/drawable-xhdpi/ic_new.png
index 447a9398b..447a9398b 100755
--- a/res/drawable-xhdpi/ic_new.png
+++ b/core/src/main/res/drawable-xhdpi/ic_new.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_new_dark.png b/core/src/main/res/drawable-xhdpi/ic_new_dark.png
index 4a23d309c..4a23d309c 100755
--- a/res/drawable-xhdpi/ic_new_dark.png
+++ b/core/src/main/res/drawable-xhdpi/ic_new_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_stat_antenna.png b/core/src/main/res/drawable-xhdpi/ic_stat_antenna_default.png
index 50d73271d..50d73271d 100644
--- a/res/drawable-xhdpi/ic_stat_antenna.png
+++ b/core/src/main/res/drawable-xhdpi/ic_stat_antenna_default.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_stat_authentication.png b/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png
index 4adfb636c..4adfb636c 100755
--- a/res/drawable-xhdpi/ic_stat_authentication.png
+++ b/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_undobar_undo.png b/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png
index 91c8429ad..91c8429ad 100644
--- a/res/drawable-xhdpi/ic_undobar_undo.png
+++ b/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png
Binary files differ
diff --git a/res/drawable-xhdpi/location_web_site.png b/core/src/main/res/drawable-xhdpi/location_web_site.png
index bd6b8682a..bd6b8682a 100644
--- a/res/drawable-xhdpi/location_web_site.png
+++ b/core/src/main/res/drawable-xhdpi/location_web_site.png
Binary files differ
diff --git a/res/drawable-xhdpi/location_web_site_dark.png b/core/src/main/res/drawable-xhdpi/location_web_site_dark.png
index 9b77be967..9b77be967 100755
--- a/res/drawable-xhdpi/location_web_site_dark.png
+++ b/core/src/main/res/drawable-xhdpi/location_web_site_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_accept.png b/core/src/main/res/drawable-xhdpi/navigation_accept.png
index b8915716e..b8915716e 100644
--- a/res/drawable-xhdpi/navigation_accept.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_accept.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_accept_dark.png b/core/src/main/res/drawable-xhdpi/navigation_accept_dark.png
index b52dc3701..b52dc3701 100755
--- a/res/drawable-xhdpi/navigation_accept_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_accept_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_cancel.png b/core/src/main/res/drawable-xhdpi/navigation_cancel.png
index ca7d159fd..ca7d159fd 100644
--- a/res/drawable-xhdpi/navigation_cancel.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_cancel.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_cancel_dark.png b/core/src/main/res/drawable-xhdpi/navigation_cancel_dark.png
index f391760ef..f391760ef 100755
--- a/res/drawable-xhdpi/navigation_cancel_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_cancel_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_chapters.png b/core/src/main/res/drawable-xhdpi/navigation_chapters.png
index d527454c6..d527454c6 100755
--- a/res/drawable-xhdpi/navigation_chapters.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_chapters.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_chapters_dark.png b/core/src/main/res/drawable-xhdpi/navigation_chapters_dark.png
index e53d5eb16..e53d5eb16 100755
--- a/res/drawable-xhdpi/navigation_chapters_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_chapters_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_collapse.png b/core/src/main/res/drawable-xhdpi/navigation_collapse.png
index be6a7688c..be6a7688c 100755
--- a/res/drawable-xhdpi/navigation_collapse.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_collapse.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_collapse_dark.png b/core/src/main/res/drawable-xhdpi/navigation_collapse_dark.png
index 2ed325108..2ed325108 100755
--- a/res/drawable-xhdpi/navigation_collapse_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_collapse_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_expand.png b/core/src/main/res/drawable-xhdpi/navigation_expand.png
index 53c013b09..53c013b09 100644
--- a/res/drawable-xhdpi/navigation_expand.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_expand.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_expand_dark.png b/core/src/main/res/drawable-xhdpi/navigation_expand_dark.png
index 38c7b20d7..38c7b20d7 100755
--- a/res/drawable-xhdpi/navigation_expand_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_expand_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_refresh.png b/core/src/main/res/drawable-xhdpi/navigation_refresh.png
index e6212cf67..e6212cf67 100644
--- a/res/drawable-xhdpi/navigation_refresh.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_refresh.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_refresh_dark.png b/core/src/main/res/drawable-xhdpi/navigation_refresh_dark.png
index a7fdc0dfc..a7fdc0dfc 100755
--- a/res/drawable-xhdpi/navigation_refresh_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_refresh_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_shownotes.png b/core/src/main/res/drawable-xhdpi/navigation_shownotes.png
index a0a156a94..a0a156a94 100755
--- a/res/drawable-xhdpi/navigation_shownotes.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_shownotes.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_shownotes_dark.png b/core/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png
index 95708234a..95708234a 100755
--- a/res/drawable-xhdpi/navigation_shownotes_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_up.png b/core/src/main/res/drawable-xhdpi/navigation_up.png
index f8c3e6f75..f8c3e6f75 100755
--- a/res/drawable-xhdpi/navigation_up.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_up.png
Binary files differ
diff --git a/res/drawable-xhdpi/navigation_up_dark.png b/core/src/main/res/drawable-xhdpi/navigation_up_dark.png
index 6964e069b..6964e069b 100755
--- a/res/drawable-xhdpi/navigation_up_dark.png
+++ b/core/src/main/res/drawable-xhdpi/navigation_up_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/social_share.png b/core/src/main/res/drawable-xhdpi/social_share.png
index cdafd8abc..cdafd8abc 100644
--- a/res/drawable-xhdpi/social_share.png
+++ b/core/src/main/res/drawable-xhdpi/social_share.png
Binary files differ
diff --git a/res/drawable-xhdpi/social_share_dark.png b/core/src/main/res/drawable-xhdpi/social_share_dark.png
index 15549b04e..15549b04e 100755
--- a/res/drawable-xhdpi/social_share_dark.png
+++ b/core/src/main/res/drawable-xhdpi/social_share_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/spinner_button.9.png b/core/src/main/res/drawable-xhdpi/spinner_button.9.png
index 3dc481e54..3dc481e54 100644
--- a/res/drawable-xhdpi/spinner_button.9.png
+++ b/core/src/main/res/drawable-xhdpi/spinner_button.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/spinner_button_dark.9.png b/core/src/main/res/drawable-xhdpi/spinner_button_dark.9.png
index c43293d5c..c43293d5c 100644
--- a/res/drawable-xhdpi/spinner_button_dark.9.png
+++ b/core/src/main/res/drawable-xhdpi/spinner_button_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/stat_playlist.png b/core/src/main/res/drawable-xhdpi/stat_playlist.png
index 7977e6f2a..7977e6f2a 100644
--- a/res/drawable-xhdpi/stat_playlist.png
+++ b/core/src/main/res/drawable-xhdpi/stat_playlist.png
Binary files differ
diff --git a/res/drawable-xhdpi/stat_playlist_dark.png b/core/src/main/res/drawable-xhdpi/stat_playlist_dark.png
index f32dd3780..f32dd3780 100644
--- a/res/drawable-xhdpi/stat_playlist_dark.png
+++ b/core/src/main/res/drawable-xhdpi/stat_playlist_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/type_audio.png b/core/src/main/res/drawable-xhdpi/type_audio.png
index 777fab84e..777fab84e 100644
--- a/res/drawable-xhdpi/type_audio.png
+++ b/core/src/main/res/drawable-xhdpi/type_audio.png
Binary files differ
diff --git a/res/drawable-xhdpi/type_audio_dark.png b/core/src/main/res/drawable-xhdpi/type_audio_dark.png
index dfd2b33c7..dfd2b33c7 100755
--- a/res/drawable-xhdpi/type_audio_dark.png
+++ b/core/src/main/res/drawable-xhdpi/type_audio_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/type_video.png b/core/src/main/res/drawable-xhdpi/type_video.png
index bbd1f112f..bbd1f112f 100644
--- a/res/drawable-xhdpi/type_video.png
+++ b/core/src/main/res/drawable-xhdpi/type_video.png
Binary files differ
diff --git a/res/drawable-xhdpi/type_video_dark.png b/core/src/main/res/drawable-xhdpi/type_video_dark.png
index a74947459..a74947459 100755
--- a/res/drawable-xhdpi/type_video_dark.png
+++ b/core/src/main/res/drawable-xhdpi/type_video_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/undobar.9.png b/core/src/main/res/drawable-xhdpi/undobar.9.png
index 22fa2205b..22fa2205b 100644
--- a/res/drawable-xhdpi/undobar.9.png
+++ b/core/src/main/res/drawable-xhdpi/undobar.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/undobar_button_focused.9.png b/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png
index d284ca7cb..d284ca7cb 100644
--- a/res/drawable-xhdpi/undobar_button_focused.9.png
+++ b/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/undobar_button_pressed.9.png b/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png
index e990659f0..e990659f0 100644
--- a/res/drawable-xhdpi/undobar_button_pressed.9.png
+++ b/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/undobar_divider.9.png b/core/src/main/res/drawable-xhdpi/undobar_divider.9.png
index 1b067d4e7..1b067d4e7 100644
--- a/res/drawable-xhdpi/undobar_divider.9.png
+++ b/core/src/main/res/drawable-xhdpi/undobar_divider.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_action_overflow.png b/core/src/main/res/drawable-xxhdpi/ic_action_overflow.png
index 5a603b6bc..5a603b6bc 100644
--- a/res/drawable-xxhdpi/ic_action_overflow.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_action_overflow.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png
index e22049b1e..e22049b1e 100644
--- a/res/drawable-xxhdpi/ic_action_overflow_dark.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png
index fa85601cf..fa85601cf 100755
--- a/res/drawable-xxhdpi/ic_action_pause_over_video.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png
index 121be211e..121be211e 100755
--- a/res/drawable-xxhdpi/ic_action_play_over_video.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png
index f834699c6..f834699c6 100755
--- a/res/drawable-xxhdpi/ic_drag_handle.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png
index a9408bc9d..a9408bc9d 100755
--- a/res/drawable-xxhdpi/ic_drag_handle_dark.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drawer.png b/core/src/main/res/drawable-xxhdpi/ic_drawer.png
index 9c4685d6e..9c4685d6e 100644
--- a/res/drawable-xxhdpi/ic_drawer.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_drawer.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drawer_dark.png b/core/src/main/res/drawable-xxhdpi/ic_drawer_dark.png
index f7e3b3079..f7e3b3079 100644
--- a/res/drawable-xxhdpi/ic_drawer_dark.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_drawer_dark.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_launcher.png b/core/src/main/res/drawable-xxhdpi/ic_launcher.png
index 2bef52ec7..2bef52ec7 100644
--- a/res/drawable-xxhdpi/ic_launcher.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_new.png b/core/src/main/res/drawable-xxhdpi/ic_new.png
index 5e836eae4..5e836eae4 100755
--- a/res/drawable-xxhdpi/ic_new.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_new.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_new_dark.png b/core/src/main/res/drawable-xxhdpi/ic_new_dark.png
index bca96b751..bca96b751 100755
--- a/res/drawable-xxhdpi/ic_new_dark.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_new_dark.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_stat_authentication.png b/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png
index b274bb60f..b274bb60f 100755
--- a/res/drawable-xxhdpi/ic_stat_authentication.png
+++ b/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png
Binary files differ
diff --git a/res/drawable/badge.xml b/core/src/main/res/drawable/badge.xml
index f98384cb9..f98384cb9 100644
--- a/res/drawable/badge.xml
+++ b/core/src/main/res/drawable/badge.xml
diff --git a/res/drawable/borderless_button.xml b/core/src/main/res/drawable/borderless_button.xml
index 27d723eed..27d723eed 100644
--- a/res/drawable/borderless_button.xml
+++ b/core/src/main/res/drawable/borderless_button.xml
diff --git a/res/drawable/borderless_button_dark.xml b/core/src/main/res/drawable/borderless_button_dark.xml
index 6d263938d..6d263938d 100644
--- a/res/drawable/borderless_button_dark.xml
+++ b/core/src/main/res/drawable/borderless_button_dark.xml
diff --git a/res/drawable/horizontal_divider.9.png b/core/src/main/res/drawable/horizontal_divider.9.png
index 7db0549da..7db0549da 100644
--- a/res/drawable/horizontal_divider.9.png
+++ b/core/src/main/res/drawable/horizontal_divider.9.png
Binary files differ
diff --git a/res/drawable/overlay_button_circle_background.xml b/core/src/main/res/drawable/overlay_button_circle_background.xml
index 90c51472c..90c51472c 100644
--- a/res/drawable/overlay_button_circle_background.xml
+++ b/core/src/main/res/drawable/overlay_button_circle_background.xml
diff --git a/res/drawable/overlay_drawable.xml b/core/src/main/res/drawable/overlay_drawable.xml
index 185ffefc1..185ffefc1 100644
--- a/res/drawable/overlay_drawable.xml
+++ b/core/src/main/res/drawable/overlay_drawable.xml
diff --git a/res/drawable/overlay_drawable_dark.xml b/core/src/main/res/drawable/overlay_drawable_dark.xml
index fb78f5633..fb78f5633 100644
--- a/res/drawable/overlay_drawable_dark.xml
+++ b/core/src/main/res/drawable/overlay_drawable_dark.xml
diff --git a/res/drawable/type_audio.png b/core/src/main/res/drawable/type_audio.png
index 4ec9efd97..4ec9efd97 100644
--- a/res/drawable/type_audio.png
+++ b/core/src/main/res/drawable/type_audio.png
Binary files differ
diff --git a/res/drawable/type_video.png b/core/src/main/res/drawable/type_video.png
index a2722b812..a2722b812 100644
--- a/res/drawable/type_video.png
+++ b/core/src/main/res/drawable/type_video.png
Binary files differ
diff --git a/res/drawable/undobar_button.xml b/core/src/main/res/drawable/undobar_button.xml
index a4de91b49..a4de91b49 100644
--- a/res/drawable/undobar_button.xml
+++ b/core/src/main/res/drawable/undobar_button.xml
diff --git a/res/drawable/vertical_divider.9.png b/core/src/main/res/drawable/vertical_divider.9.png
index 6a0edafb3..6a0edafb3 100644
--- a/res/drawable/vertical_divider.9.png
+++ b/core/src/main/res/drawable/vertical_divider.9.png
Binary files differ
diff --git a/res/drawable/white_circle.xml b/core/src/main/res/drawable/white_circle.xml
index 597b70a2d..597b70a2d 100644
--- a/res/drawable/white_circle.xml
+++ b/core/src/main/res/drawable/white_circle.xml
diff --git a/res/values-az/strings.xml b/core/src/main/res/values-az/strings.xml
index adb983e9e..adb983e9e 100644
--- a/res/values-az/strings.xml
+++ b/core/src/main/res/values-az/strings.xml
diff --git a/res/values-ca/strings.xml b/core/src/main/res/values-ca/strings.xml
index 20f96ae23..20f96ae23 100644
--- a/res/values-ca/strings.xml
+++ b/core/src/main/res/values-ca/strings.xml
diff --git a/res/values-cs-rCZ/strings.xml b/core/src/main/res/values-cs-rCZ/strings.xml
index f7844ed8c..f7844ed8c 100644
--- a/res/values-cs-rCZ/strings.xml
+++ b/core/src/main/res/values-cs-rCZ/strings.xml
diff --git a/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml
index 4e5ccd255..4e5ccd255 100644
--- a/res/values-da/strings.xml
+++ b/core/src/main/res/values-da/strings.xml
diff --git a/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml
index 3947582fc..3947582fc 100644
--- a/res/values-de/strings.xml
+++ b/core/src/main/res/values-de/strings.xml
diff --git a/res/values-es-rES/strings.xml b/core/src/main/res/values-es-rES/strings.xml
index cd4949530..cd4949530 100644
--- a/res/values-es-rES/strings.xml
+++ b/core/src/main/res/values-es-rES/strings.xml
diff --git a/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml
index b00c9df17..b00c9df17 100644
--- a/res/values-es/strings.xml
+++ b/core/src/main/res/values-es/strings.xml
diff --git a/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml
index 0d3889468..0d3889468 100644
--- a/res/values-fr/strings.xml
+++ b/core/src/main/res/values-fr/strings.xml
diff --git a/res/values-hi-rIN/strings.xml b/core/src/main/res/values-hi-rIN/strings.xml
index b1d6c33b1..b1d6c33b1 100644
--- a/res/values-hi-rIN/strings.xml
+++ b/core/src/main/res/values-hi-rIN/strings.xml
diff --git a/res/values-it-rIT/strings.xml b/core/src/main/res/values-it-rIT/strings.xml
index b91f1a5fd..b91f1a5fd 100644
--- a/res/values-it-rIT/strings.xml
+++ b/core/src/main/res/values-it-rIT/strings.xml
diff --git a/res/values-iw-rIL/strings.xml b/core/src/main/res/values-iw-rIL/strings.xml
index 82a8fe773..82a8fe773 100644
--- a/res/values-iw-rIL/strings.xml
+++ b/core/src/main/res/values-iw-rIL/strings.xml
diff --git a/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml
index a29de1582..a29de1582 100644
--- a/res/values-ko/strings.xml
+++ b/core/src/main/res/values-ko/strings.xml
diff --git a/res/values-land/styles.xml b/core/src/main/res/values-land/styles.xml
index d964ef3d4..d964ef3d4 100644
--- a/res/values-land/styles.xml
+++ b/core/src/main/res/values-land/styles.xml
diff --git a/res/values-large/dimens.xml b/core/src/main/res/values-large/dimens.xml
index 27b4868c7..27b4868c7 100644
--- a/res/values-large/dimens.xml
+++ b/core/src/main/res/values-large/dimens.xml
diff --git a/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml
index 9df199324..9df199324 100644
--- a/res/values-nl/strings.xml
+++ b/core/src/main/res/values-nl/strings.xml
diff --git a/res/values-pl-rPL/strings.xml b/core/src/main/res/values-pl-rPL/strings.xml
index ed8abb227..ed8abb227 100644
--- a/res/values-pl-rPL/strings.xml
+++ b/core/src/main/res/values-pl-rPL/strings.xml
diff --git a/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml
index ef63e718e..ef63e718e 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/core/src/main/res/values-pt-rBR/strings.xml
diff --git a/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml
index d43845ae4..d43845ae4 100644
--- a/res/values-pt/strings.xml
+++ b/core/src/main/res/values-pt/strings.xml
diff --git a/res/values-ro-rRO/strings.xml b/core/src/main/res/values-ro-rRO/strings.xml
index 6cc93e4a9..6cc93e4a9 100644
--- a/res/values-ro-rRO/strings.xml
+++ b/core/src/main/res/values-ro-rRO/strings.xml
diff --git a/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml
index 400c3a543..400c3a543 100644
--- a/res/values-ru/strings.xml
+++ b/core/src/main/res/values-ru/strings.xml
diff --git a/res/values-sv-rSE/strings.xml b/core/src/main/res/values-sv-rSE/strings.xml
index 622741dc5..622741dc5 100644
--- a/res/values-sv-rSE/strings.xml
+++ b/core/src/main/res/values-sv-rSE/strings.xml
diff --git a/res/values-uk-rUA/strings.xml b/core/src/main/res/values-uk-rUA/strings.xml
index 64f5c38af..64f5c38af 100644
--- a/res/values-uk-rUA/strings.xml
+++ b/core/src/main/res/values-uk-rUA/strings.xml
diff --git a/res/values-v11/colors.xml b/core/src/main/res/values-v11/colors.xml
index 520efaa06..520efaa06 100644
--- a/res/values-v11/colors.xml
+++ b/core/src/main/res/values-v11/colors.xml
diff --git a/res/values-v14/dimens.xml b/core/src/main/res/values-v14/dimens.xml
index 090a476a8..090a476a8 100644
--- a/res/values-v14/dimens.xml
+++ b/core/src/main/res/values-v14/dimens.xml
diff --git a/res/values-v14/styles.xml b/core/src/main/res/values-v14/styles.xml
index 6a39d6175..6a39d6175 100644
--- a/res/values-v14/styles.xml
+++ b/core/src/main/res/values-v14/styles.xml
diff --git a/res/values-v16/styles.xml b/core/src/main/res/values-v16/styles.xml
index e7c56b5f5..e7c56b5f5 100644
--- a/res/values-v16/styles.xml
+++ b/core/src/main/res/values-v16/styles.xml
diff --git a/res/values-v19/colors.xml b/core/src/main/res/values-v19/colors.xml
index 16c065d75..16c065d75 100644
--- a/res/values-v19/colors.xml
+++ b/core/src/main/res/values-v19/colors.xml
diff --git a/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml
index 59958ec35..59958ec35 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/core/src/main/res/values-zh-rCN/strings.xml
diff --git a/res/values/arrays.xml b/core/src/main/res/values/arrays.xml
index 9b9079021..9b9079021 100644
--- a/res/values/arrays.xml
+++ b/core/src/main/res/values/arrays.xml
diff --git a/res/values/attrs.xml b/core/src/main/res/values/attrs.xml
index 08a8063c1..08a8063c1 100644
--- a/res/values/attrs.xml
+++ b/core/src/main/res/values/attrs.xml
diff --git a/res/values/colors.xml b/core/src/main/res/values/colors.xml
index 6b535079d..6b535079d 100644
--- a/res/values/colors.xml
+++ b/core/src/main/res/values/colors.xml
diff --git a/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
index 1ebcdb76d..1ebcdb76d 100644
--- a/res/values/dimens.xml
+++ b/core/src/main/res/values/dimens.xml
diff --git a/res/values/ids.xml b/core/src/main/res/values/ids.xml
index 90e405fde..90e405fde 100644
--- a/res/values/ids.xml
+++ b/core/src/main/res/values/ids.xml
diff --git a/res/values/integers.xml b/core/src/main/res/values/integers.xml
index 33501d9fb..33501d9fb 100644
--- a/res/values/integers.xml
+++ b/core/src/main/res/values/integers.xml
diff --git a/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 6a62f5e14..6a62f5e14 100644
--- a/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
diff --git a/res/values/styles.xml b/core/src/main/res/values/styles.xml
index e42072afa..e42072afa 100644
--- a/res/values/styles.xml
+++ b/core/src/main/res/values/styles.xml
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 6c3897a9c..b6dc96bf1 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sun Sep 28 21:26:43 CEST 2014
+#Sun Oct 12 21:44:21 CEST 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index c90a33f1c..000000000
--- a/pom.xml
+++ /dev/null
@@ -1,269 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>de.danoeh</groupId>
- <artifactId>antennapod</artifactId>
- <packaging>apk</packaging>
- <version>0.9.9.4</version>
- <name>AntennaPod</name>
-
-
- <dependencies>
- <dependency>
- <groupId>android.support</groupId>
- <artifactId>compatibility-v4</artifactId>
- <version>19</version>
- </dependency>
- <dependency>
- <groupId>android.support</groupId>
- <artifactId>compatibility-v7-appcompat</artifactId>
- <version>19</version>
- <type>apklib</type>
- </dependency>
- <dependency>
- <groupId>android.support</groupId>
- <artifactId>compatibility-v7-appcompat</artifactId>
- <version>19</version>
- <type>jar</type>
- </dependency>
- <dependency>
- <groupId>com.google.android</groupId>
- <artifactId>android-test</artifactId>
- <version>2.2.1</version>
- </dependency>
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
- <version>3.1</version>
- </dependency>
- <dependency>
- <groupId>org.shredzone.flattr4j</groupId>
- <artifactId>flattr4j-core</artifactId>
- <version>2.7</version>
- <scope>compile</scope>
- <exclusions>
- <exclusion>
- <groupId>org.apache.httpcomponents</groupId>
- <artifactId>httpcore</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.apache.httpcomponents</groupId>
- <artifactId>httpclient</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.json</groupId>
- <artifactId>json</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>android</groupId>
- <artifactId>android</artifactId>
- <scope>provided</scope>
- <version>4.4_r1</version>
- </dependency>
- <dependency>
- <groupId>com.google.android.annotations</groupId>
- <artifactId>annotations</artifactId>
- <version>22.3</version>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <version>2.4</version>
- </dependency>
- <dependency>
- <groupId>com.mobeta.android.dslv</groupId>
- <artifactId>drag-sort-listview</artifactId>
- <version>0.6.1-SNAPSHOT</version>
- <type>apklib</type>
- </dependency>
- <dependency>
- <groupId>com.nineoldandroids</groupId>
- <artifactId>library</artifactId>
- <version>2.4.0</version>
- </dependency>
- <dependency>
- <groupId>com.aocate</groupId>
- <artifactId>presto_client</artifactId>
- <version>0.8.5</version>
- <type>jar</type>
- <scope>system</scope>
- <systemPath>${project.basedir}/libs/presto_client-0.8.5.jar</systemPath>
- </dependency>
- </dependencies>
-
- <build>
- <sourceDirectory>src</sourceDirectory>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>2.3.2</version>
- <configuration>
- <source>1.6</source>
- <target>1.6</target>
- </configuration>
- </plugin>
- <plugin>
- <groupId>com.jayway.maven.plugins.android.generation2</groupId>
- <artifactId>android-maven-plugin</artifactId>
- <version>3.8.0</version>
- <configuration>
- <sdk>
- <path>${env.ANDROID_HOME}</path>
- <platform>19</platform>
- </sdk>
- <manifest>
- <debuggable>true</debuggable>
- </manifest>
- </configuration>
- <extensions>true</extensions>
- <executions>
- <execution>
- <id>alignApk</id>
- <phase>package</phase>
- <goals>
- <goal>zipalign</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-
- <profiles>
- <profile>
- <id>development</id>
- <!-- using this since activeByDefault does not work well with multiple
- profiles -->
- <activation>
- <property>
- <name>environment</name>
- <value>!production</value>
- </property>
- </activation>
- <properties>
- <deployment.stage>In Development</deployment.stage>
- </properties>
- </profile>
- <profile>
- <id>production</id>
- <properties>
- <deployment.stage>In Production</deployment.stage>
- </properties>
- </profile>
- <profile>
- <id>release</id>
- <!-- via this activation the profile is automatically used when the release
- is done with the maven release plugin -->
- <activation>
- <property>
- <name>performRelease</name>
- <value>true</value>
- </property>
- </activation>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jarsigner-plugin</artifactId>
- <executions>
- <execution>
- <id>signing</id>
- <goals>
- <goal>sign</goal>
- <goal>verify</goal>
- </goals>
- <phase>package</phase>
- <inherited>true</inherited>
- <configuration>
- <removeExistingSignatures>true</removeExistingSignatures>
- <archiveDirectory />
- <includes>
- <include>${project.build.directory}/${project.artifactId}-${project.version}.apk</include>
- </includes>
- <keystore>${sign.keystore}</keystore>
- <alias>${sign.alias}</alias>
- <storepass>${sign.storepass}</storepass>
- <keypass>${sign.keypass}</keypass>
- <verbose>true</verbose>
- <arguments>
- <argument>-sigalg</argument><argument>MD5withRSA</argument>
- <argument>-digestalg</argument><argument>SHA1</argument>
- </arguments>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <!-- the signed apk then needs to be zipaligned and we activate proguard
- and we run the manifest update -->
- <plugin>
- <groupId>com.jayway.maven.plugins.android.generation2</groupId>
- <artifactId>android-maven-plugin</artifactId>
- <inherited>true</inherited>
- <configuration>
- <sign>
- <debug>false</debug>
- </sign>
- <zipalign>
- <skip>false</skip>
- <verbose>true</verbose>
- <inputApk>${project.build.directory}/${project.artifactId}-${project.version}.apk</inputApk>
- <outputApk>${project.build.directory}/${project.artifactId}-${project.version}-signed-aligned.apk
- </outputApk>
- </zipalign>
- <manifest>
- <debuggable>false</debuggable>
- <versionCodeAutoIncrement>false</versionCodeAutoIncrement>
- </manifest>
- <proguard>
- <skip>false</skip>
- <config>proguard-mvn.cfg</config>
- </proguard>
- </configuration>
- <executions>
- <execution>
- <id>alignApk</id>
- <phase>package</phase>
- <goals>
- <goal>zipalign</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>build-helper-maven-plugin</artifactId>
- <configuration>
- <artifacts>
- <artifact>
- <file>${project.build.directory}/${project.artifactId}-${project.version}-signed-aligned.apk</file>
- <type>apk</type>
- <classifier>signed-aligned</classifier>
- </artifact>
- <artifact>
- <file>${project.build.directory}/proguard/mapping.txt</file>
- <type>map</type>
- <classifier>release</classifier>
- </artifact>
- </artifacts>
- </configuration>
- <executions>
- <execution>
- <id>attach-signed-aligned</id>
- <phase>package</phase>
- <goals>
- <goal>attach-artifact</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- </profile>
- </profiles>
-
-</project>
diff --git a/proguard-mvn.cfg b/proguard-mvn.cfg
deleted file mode 100644
index 70019bfcb..000000000
--- a/proguard-mvn.cfg
+++ /dev/null
@@ -1,66 +0,0 @@
--printmapping out.map
--renamesourcefileattribute SourceFile
--keepattributes SourceFile,LineNumberTable
-
--dontpreverify
--repackageclasses ''
--allowaccessmodification
--optimizations !code/simplification/arithmetic
--keepattributes *Annotation*
-
--injars libs/presto_client-0.8.5.jar
-
--keep public class * extends android.app.Activity
--keep public class * extends android.app.Application
--keep public class * extends android.app.Service
--keep public class * extends android.content.BroadcastReceiver
--keep public class * extends android.content.ContentProvider
-
--keep public class * extends android.view.View {
- public <init>(android.content.Context);
- public <init>(android.content.Context, android.util.AttributeSet);
- public <init>(android.content.Context, android.util.AttributeSet, int);
- public void set*(...);
-}
-
--keepclassmembers enum * {
- public static **[] values();
- public static ** valueOf(java.lang.String);
-}
-
--keepclasseswithmembers class * {
- public <init>(android.content.Context, android.util.AttributeSet);
-}
-
--keepclasseswithmembers class * {
- public <init>(android.content.Context, android.util.AttributeSet, int);
-}
-
--keepclassmembers class * extends android.content.Context {
- public void *(android.view.View);
- public void *(android.view.MenuItem);
-}
-
--keepclassmembers class * implements android.os.Parcelable {
- static android.os.Parcelable$Creator CREATOR;
-}
-
--keepclassmembers class **.R$* {
- public static <fields>;
-}
-
--keep class android.support.v4.** { *; }
--keep interface android.support.v4.** { *; }
--keep class android.support.v7.** { *; }
--keep interface android.support.v7.** { *; }
--dontwarn android.support.v4.**
--dontwarn android.support.v7.**
-
--keepattributes *Annotation*
-
--keep class org.shredzone.flattr4j.** { *; }
--dontwarn org.shredzone.flattr4j.**
-
--keep class org.apache.commons.** { *; }
-
--dontskipnonpubliclibraryclassmembers
diff --git a/proguard-project.txt b/proguard-project.txt
deleted file mode 100644
index f2fe1559a..000000000
--- a/proguard-project.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-# To enable ProGuard in your project, edit project.properties
-# to define the proguard.config property as described in that file.
-#
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in ${sdk.dir}/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the ProGuard
-# include property in project.properties.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
diff --git a/settings.gradle b/settings.gradle
index 6de43d84c..de34bc1c1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,2 @@
-include ':submodules:dslv:library'
+include ':app', ':core'
+include ':app:dslv:library'
diff --git a/src/com/aocate/media/MediaPlayer.java b/src/com/aocate/media/MediaPlayer.java
deleted file mode 100644
index 04ecd58a9..000000000
--- a/src/com/aocate/media/MediaPlayer.java
+++ /dev/null
@@ -1,1296 +0,0 @@
-// 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.
-
-package com.aocate.media;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.locks.ReentrantLock;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.media.AudioManager;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Handler.Callback;
-import android.util.Log;
-
-import de.danoeh.antennapod.BuildConfig;
-
-public class MediaPlayer {
- public interface OnBufferingUpdateListener {
- public abstract void onBufferingUpdate(MediaPlayer arg0, int percent);
- }
-
- public interface OnCompletionListener {
- public abstract void onCompletion(MediaPlayer arg0);
- }
-
- public interface OnErrorListener {
- public abstract boolean onError(MediaPlayer arg0, int what, int extra);
- }
-
- public interface OnInfoListener {
- public abstract boolean onInfo(MediaPlayer arg0, int what, int extra);
- }
-
- public interface OnPitchAdjustmentAvailableChangedListener {
- /**
- *
- * @param arg0
- * The owning media player
- * @param pitchAdjustmentAvailable
- * True if pitch adjustment is available, false if not
- */
- public abstract void onPitchAdjustmentAvailableChanged(
- MediaPlayer arg0, boolean pitchAdjustmentAvailable);
- }
-
- public interface OnPreparedListener {
- public abstract void onPrepared(MediaPlayer arg0);
- }
-
- public interface OnSeekCompleteListener {
- public abstract void onSeekComplete(MediaPlayer arg0);
- }
-
- public interface OnSpeedAdjustmentAvailableChangedListener {
- /**
- *
- * @param arg0
- * The owning media player
- * @param speedAdjustmentAvailable
- * True if speed adjustment is available, false if not
- */
- public abstract void onSpeedAdjustmentAvailableChanged(
- MediaPlayer arg0, boolean speedAdjustmentAvailable);
- }
-
- public enum State {
- IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED, PREPARING, PLAYBACK_COMPLETED, END, ERROR
- }
-
- private static Uri SPEED_ADJUSTMENT_MARKET_URI = Uri
- .parse("market://details?id=com.aocate.presto");
-
- private static Intent prestoMarketIntent = null;
-
- public static final int MEDIA_ERROR_SERVER_DIED = android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED;
- public static final int MEDIA_ERROR_UNKNOWN = android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN;
- public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = android.media.MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK;
-
- /**
- * Indicates whether the specified action can be used as an intent. This
- * method queries the package manager for installed packages that can
- * respond to an intent with the specified action. If no suitable package is
- * found, this method returns false.
- *
- * @param context
- * The application's environment.
- * @param action
- * The Intent action to check for availability.
- *
- * @return True if an Intent with the specified action can be sent and
- * responded to, false otherwise.
- */
- public static boolean isIntentAvailable(Context context, String action) {
- final PackageManager packageManager = context.getPackageManager();
- final Intent intent = new Intent(action);
- List<ResolveInfo> list = packageManager.queryIntentServices(intent,
- PackageManager.MATCH_DEFAULT_ONLY);
- return list.size() > 0;
- }
-
- /**
- * Indicates whether the Presto library is installed
- *
- * @param context
- * The context to use to query the package manager.
- * @return True if the Presto library is installed, false if not.
- */
- public static boolean isPrestoLibraryInstalled(Context context) {
- return isIntentAvailable(context, ServiceBackedMediaPlayer.INTENT_NAME);
- }
-
- /**
- * Return an Intent that opens the Android Market page for the speed
- * alteration library
- *
- * @return The Intent for the Presto library on the Android Market
- */
- public static Intent getPrestoMarketIntent() {
- if (prestoMarketIntent == null) {
- prestoMarketIntent = new Intent(Intent.ACTION_VIEW,
- SPEED_ADJUSTMENT_MARKET_URI);
- }
- return prestoMarketIntent;
- }
-
- /**
- * Open the Android Market page for the Presto library
- *
- * @param context
- * The context from which to open the Android Market page
- */
- public static void openPrestoMarketIntent(Context context) {
- context.startActivity(getPrestoMarketIntent());
- }
-
- private static final String MP_TAG = "AocateReplacementMediaPlayer";
-
- private static final double PITCH_STEP_CONSTANT = 1.0594630943593;
-
- private AndroidMediaPlayer amp = null;
- // This is whether speed adjustment should be enabled (by the Service)
- // To avoid the Service entirely, set useService to false
- protected boolean enableSpeedAdjustment = true;
- private int lastKnownPosition = 0;
- // In some cases, we're going to have to replace the
- // android.media.MediaPlayer on the fly, and we don't want to touch the
- // wrong media player, so lock it way too much.
- ReentrantLock lock = new ReentrantLock();
- private int mAudioStreamType = AudioManager.STREAM_MUSIC;
- private Context mContext;
- private boolean mIsLooping = false;
- private float mLeftVolume = 1f;
- private float mPitchStepsAdjustment = 0f;
- private float mRightVolume = 1f;
- private float mSpeedMultiplier = 1f;
- private int mWakeMode = 0;
- MediaPlayerImpl mpi = null;
- protected boolean pitchAdjustmentAvailable = false;
- private ServiceBackedMediaPlayer sbmp = null;
- protected boolean speedAdjustmentAvailable = false;
-
- private Handler mServiceDisconnectedHandler = null;
-
- // Some parts of state cannot be found by calling MediaPlayerImpl functions,
- // so store our own state. This also helps copy state when changing
- // implementations
- State state = State.INITIALIZED;
- String stringDataSource = null;
- Uri uriDataSource = null;
- private boolean useService = false;
-
- // Naming Convention for Listeners
- // Most listeners can both be set by clients and called by MediaPlayImpls
- // There are a few that have to do things in this class as well as calling
- // the function. In all cases, onX is what is called by MediaPlayerImpl
- // If there is work to be done in this class, then the listener that is
- // set by setX is X (with the first letter lowercase).
- OnBufferingUpdateListener onBufferingUpdateListener = null;
- OnCompletionListener onCompletionListener = null;
- OnErrorListener onErrorListener = null;
- OnInfoListener onInfoListener = null;
-
- // Special case. Pitch adjustment ceases to be available when we switch
- // to the android.media.MediaPlayer (though it is not guaranteed to be
- // available when using the ServiceBackedMediaPlayer)
- OnPitchAdjustmentAvailableChangedListener onPitchAdjustmentAvailableChangedListener = new OnPitchAdjustmentAvailableChangedListener() {
- public void onPitchAdjustmentAvailableChanged(MediaPlayer arg0,
- boolean pitchAdjustmentAvailable) {
- lock.lock();
- try {
- Log
- .d(
- MP_TAG,
- "onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged being called");
- if (MediaPlayer.this.pitchAdjustmentAvailable != pitchAdjustmentAvailable) {
- Log.d(MP_TAG, "Pitch adjustment state has changed from "
- + MediaPlayer.this.pitchAdjustmentAvailable
- + " to " + pitchAdjustmentAvailable);
- MediaPlayer.this.pitchAdjustmentAvailable = pitchAdjustmentAvailable;
- if (MediaPlayer.this.pitchAdjustmentAvailableChangedListener != null) {
- MediaPlayer.this.pitchAdjustmentAvailableChangedListener
- .onPitchAdjustmentAvailableChanged(arg0,
- pitchAdjustmentAvailable);
- }
- }
- } finally {
- lock.unlock();
- }
- }
- };
- OnPitchAdjustmentAvailableChangedListener pitchAdjustmentAvailableChangedListener = null;
-
- MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() {
- public void onPrepared(MediaPlayer arg0) {
- Log.d(MP_TAG, "onPreparedListener 242 setting state to PREPARED");
- MediaPlayer.this.state = State.PREPARED;
- if (MediaPlayer.this.preparedListener != null) {
- Log.d(MP_TAG, "Calling preparedListener");
- MediaPlayer.this.preparedListener.onPrepared(arg0);
- }
- Log.d(MP_TAG, "Wrap up onPreparedListener");
- }
- };
-
- OnPreparedListener preparedListener = null;
- OnSeekCompleteListener onSeekCompleteListener = null;
-
- // Special case. Speed adjustment ceases to be available when we switch
- // to the android.media.MediaPlayer (though it is not guaranteed to be
- // available when using the ServiceBackedMediaPlayer)
- OnSpeedAdjustmentAvailableChangedListener onSpeedAdjustmentAvailableChangedListener = new OnSpeedAdjustmentAvailableChangedListener() {
- public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0,
- boolean speedAdjustmentAvailable) {
- lock.lock();
- try {
- Log
- .d(
- MP_TAG,
- "onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged being called");
- if (MediaPlayer.this.speedAdjustmentAvailable != speedAdjustmentAvailable) {
- Log.d(MP_TAG, "Speed adjustment state has changed from "
- + MediaPlayer.this.speedAdjustmentAvailable
- + " to " + speedAdjustmentAvailable);
- MediaPlayer.this.speedAdjustmentAvailable = speedAdjustmentAvailable;
- if (MediaPlayer.this.speedAdjustmentAvailableChangedListener != null) {
- MediaPlayer.this.speedAdjustmentAvailableChangedListener
- .onSpeedAdjustmentAvailableChanged(arg0,
- speedAdjustmentAvailable);
- }
- }
- } finally {
- lock.unlock();
- }
- }
- };
- OnSpeedAdjustmentAvailableChangedListener speedAdjustmentAvailableChangedListener = null;
-
- private int speedAdjustmentAlgorithm = SpeedAdjustmentAlgorithm.SONIC;
-
- public MediaPlayer(final Context context) {
- this(context, true);
- }
-
- public MediaPlayer(final Context context, boolean useService) {
- this.mContext = context;
- this.useService = useService;
-
- // So here's the major problem
- // Sometimes the service won't exist or won't be connected,
- // so start with an android.media.MediaPlayer, and when
- // the service is connected, use that from then on
- this.mpi = this.amp = new AndroidMediaPlayer(this, context);
-
- // setupMpi will go get the Service, if it can, then bring that
- // implementation into sync
- Log.d(MP_TAG, "setupMpi");
- setupMpi(context);
- }
-
- private boolean invalidServiceConnectionConfiguration() {
- if (!(this.mpi instanceof ServiceBackedMediaPlayer)) {
- if (this.useService && isPrestoLibraryInstalled()) {
- // In this case, the Presto library has been installed
- // or something while playing sound
- // We could be using the service, but we're not
- Log.d(MP_TAG, "We could be using the service, but we're not 316");
- return true;
- }
- // If useService is false, then we shouldn't be using the SBMP
- // If the Presto library isn't installed, ditto
- Log.d(MP_TAG, "this.mpi is not a ServiceBackedMediaPlayer, but we couldn't use it anyway 321");
- return false;
- } else {
- if (BuildConfig.DEBUG && !(this.mpi instanceof ServiceBackedMediaPlayer)) throw new AssertionError();
- if (this.useService && isPrestoLibraryInstalled()) {
- // We should be using the service, and we are. Great!
- Log.d(MP_TAG, "We could be using a ServiceBackedMediaPlayer and we are 327");
- return false;
- }
- // We're trying to use the service when we shouldn't,
- // that's an invalid configuration
- Log.d(MP_TAG, "We're trying to use a ServiceBackedMediaPlayer but we shouldn't be 332");
- return true;
- }
- }
-
- private void setupMpi(final Context context) {
- lock.lock();
- try {
- Log.d(MP_TAG, "setupMpi 336");
- // Check if the client wants to use the service at all,
- // then if we're already using the right kind of media player
- if (this.useService && isPrestoLibraryInstalled()) {
- if ((this.mpi != null)
- && (this.mpi instanceof ServiceBackedMediaPlayer)) {
- Log.d(MP_TAG, "Already using ServiceBackedMediaPlayer");
- return;
- }
- if (this.sbmp == null) {
- Log.d(MP_TAG, "Instantiating new ServiceBackedMediaPlayer 346");
- this.sbmp = new ServiceBackedMediaPlayer(this, context,
- new ServiceConnection() {
- public void onServiceConnected(
- ComponentName className,
- final IBinder service) {
- Thread t = new Thread(new Runnable() {
- public void run() {
- // This lock probably isn't granular
- // enough
- MediaPlayer.this.lock.lock();
- Log.d(MP_TAG,
- "onServiceConnected 257");
- try {
- MediaPlayer.this
- .switchMediaPlayerImpl(
- MediaPlayer.this.amp,
- MediaPlayer.this.sbmp);
- Log.d(MP_TAG, "End onServiceConnected 362");
- } finally {
- MediaPlayer.this.lock.unlock();
- }
- }
- });
- t.start();
- }
-
- public void onServiceDisconnected(
- ComponentName className) {
- MediaPlayer.this.lock.lock();
- try {
- // Can't get any more useful information
- // out of sbmp
- if (MediaPlayer.this.sbmp != null) {
- MediaPlayer.this.sbmp.release();
- }
- // Unlike most other cases, sbmp gets set
- // to null since there's nothing useful
- // backing it now
- MediaPlayer.this.sbmp = null;
-
- if (mServiceDisconnectedHandler == null) {
- mServiceDisconnectedHandler = new Handler(new Callback() {
- public boolean handleMessage(Message msg) {
- // switchMediaPlayerImpl won't try to
- // clone anything from null
- lock.lock();
- try {
- if (MediaPlayer.this.amp == null) {
- // This should never be in this state
- MediaPlayer.this.amp = new AndroidMediaPlayer(
- MediaPlayer.this,
- MediaPlayer.this.mContext);
- }
- // Use sbmp instead of null in case by some miracle it's
- // been restored in the meantime
- MediaPlayer.this.switchMediaPlayerImpl(
- MediaPlayer.this.sbmp,
- MediaPlayer.this.amp);
- return true;
- }
- finally {
- lock.unlock();
- }
- }
- });
- }
-
- // This code needs to execute on the
- // original thread to instantiate
- // the new object in the right place
- mServiceDisconnectedHandler
- .sendMessage(
- mServiceDisconnectedHandler
- .obtainMessage());
- // Note that we do NOT want to set
- // useService. useService is about
- // what the user wants, not what they
- // get
- } finally {
- MediaPlayer.this.lock.unlock();
- }
- }
- }
- );
- }
- switchMediaPlayerImpl(this.amp, this.sbmp);
- } else {
- if ((this.mpi != null)
- && (this.mpi instanceof AndroidMediaPlayer)) {
- Log.d(MP_TAG, "Already using AndroidMediaPlayer");
- return;
- }
- if (this.amp == null) {
- Log.d(MP_TAG, "Instantiating new AndroidMediaPlayer (this should be impossible)");
- this.amp = new AndroidMediaPlayer(this, context);
- }
- switchMediaPlayerImpl(this.sbmp, this.amp);
- }
- } finally {
- lock.unlock();
- }
- }
-
- private void switchMediaPlayerImpl(MediaPlayerImpl from, MediaPlayerImpl to) {
- lock.lock();
- try {
- Log.d(MP_TAG, "switchMediaPlayerImpl");
- if ((from == to)
- // Same object, nothing to synchronize
- || (to == null)
- // Nothing to copy to (maybe this should throw an error?)
- || ((to instanceof ServiceBackedMediaPlayer) && !((ServiceBackedMediaPlayer) to).isConnected())
- // ServiceBackedMediaPlayer hasn't yet connected, onServiceConnected will take care of the transition
- || (MediaPlayer.this.state == State.END)) {
- // State.END is after a release(), no further functions should
- // be called on this class and from is likely to have problems
- // retrieving state that won't be used anyway
- return;
- }
- // Extract all that we can from the existing implementation
- // and copy it to the new implementation
-
- Log.d(MP_TAG, "switchMediaPlayerImpl(), current state is "
- + this.state.toString());
-
- to.reset();
-
- // Do this first so we don't have to prepare the same
- // data file twice
- to.setEnableSpeedAdjustment(MediaPlayer.this.enableSpeedAdjustment);
-
- // This is a reasonable place to set all of these,
- // none of them require prepare() or the like first
- to.setAudioStreamType(this.mAudioStreamType);
- to.setSpeedAdjustmentAlgorithm(this.speedAdjustmentAlgorithm);
- to.setLooping(this.mIsLooping);
- to.setPitchStepsAdjustment(this.mPitchStepsAdjustment);
- Log.d(MP_TAG, "Setting playback speed to " + this.mSpeedMultiplier);
- to.setPlaybackSpeed(this.mSpeedMultiplier);
- to.setVolume(MediaPlayer.this.mLeftVolume,
- MediaPlayer.this.mRightVolume);
- to.setWakeMode(this.mContext, this.mWakeMode);
-
- Log.d(MP_TAG, "asserting at least one data source is null");
- assert ((MediaPlayer.this.stringDataSource == null) || (MediaPlayer.this.uriDataSource == null));
-
- if (uriDataSource != null) {
- Log.d(MP_TAG, "switchMediaPlayerImpl(): uriDataSource != null");
- try {
- to.setDataSource(this.mContext, uriDataSource);
- } catch (IllegalArgumentException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IllegalStateException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- if (stringDataSource != null) {
- Log.d(MP_TAG,
- "switchMediaPlayerImpl(): stringDataSource != null");
- try {
- to.setDataSource(stringDataSource);
- } catch (IllegalArgumentException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IllegalStateException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- if ((this.state == State.PREPARED)
- || (this.state == State.PREPARING)
- || (this.state == State.PAUSED)
- || (this.state == State.STOPPED)
- || (this.state == State.STARTED)
- || (this.state == State.PLAYBACK_COMPLETED)) {
- Log.d(MP_TAG, "switchMediaPlayerImpl(): prepare and seek");
- // Use prepare here instead of prepareAsync so that
- // we wait for it to be ready before we try to use it
- try {
- to.muteNextOnPrepare();
- to.prepare();
- } catch (IllegalStateException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- int seekPos = 0;
- if (from != null) {
- seekPos = from.getCurrentPosition();
- } else if (this.lastKnownPosition < to.getDuration()) {
- // This can happen if the Service unexpectedly
- // disconnected. Because it would result in too much
- // information being passed around, we don't constantly
- // poll for the lastKnownPosition, but we'll save it
- // when getCurrentPosition is called
- seekPos = this.lastKnownPosition;
- }
- to.muteNextSeek();
- to.seekTo(seekPos);
- }
- if ((from != null)
- && from.isPlaying()) {
- from.pause();
- }
- if ((this.state == State.STARTED)
- || (this.state == State.PAUSED)
- || (this.state == State.STOPPED)) {
- Log.d(MP_TAG, "switchMediaPlayerImpl(): start");
- if (to != null) {
- to.start();
- }
- }
-
- if (this.state == State.PAUSED) {
- Log.d(MP_TAG, "switchMediaPlayerImpl(): paused");
- if (to != null) {
- to.pause();
- }
- } else if (this.state == State.STOPPED) {
- Log.d(MP_TAG, "switchMediaPlayerImpl(): stopped");
- if (to != null) {
- to.stop();
- }
- }
-
- this.mpi = to;
-
- // Cheating here by relying on the side effect in
- // on(Pitch|Speed)AdjustmentAvailableChanged
- if ((to.canSetPitch() != this.pitchAdjustmentAvailable)
- && (this.onPitchAdjustmentAvailableChangedListener != null)) {
- this.onPitchAdjustmentAvailableChangedListener
- .onPitchAdjustmentAvailableChanged(this, to
- .canSetPitch());
- }
- if ((to.canSetSpeed() != this.speedAdjustmentAvailable)
- && (this.onSpeedAdjustmentAvailableChangedListener != null)) {
- this.onSpeedAdjustmentAvailableChangedListener
- .onSpeedAdjustmentAvailableChanged(this, to
- .canSetSpeed());
- }
- Log.d(MP_TAG, "switchMediaPlayerImpl() 625 " + this.state.toString());
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Returns true if pitch can be changed at this moment
- *
- * @return True if pitch can be changed
- */
- public boolean canSetPitch() {
- lock.lock();
- try {
- return this.mpi.canSetPitch();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Returns true if speed can be changed at this moment
- *
- * @return True if speed can be changed
- */
- public boolean canSetSpeed() {
- lock.lock();
- try {
- return this.mpi.canSetSpeed();
- } finally {
- lock.unlock();
- }
- }
-
- protected void finalize() throws Throwable {
- lock.lock();
- try {
- Log.d(MP_TAG, "finalize() 626");
- this.release();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Returns the number of steps (in a musical scale) by which playback is
- * currently shifted. When greater than zero, pitch is shifted up. When less
- * than zero, pitch is shifted down.
- *
- * @return The number of steps pitch is currently shifted by
- */
- public float getCurrentPitchStepsAdjustment() {
- lock.lock();
- try {
- return this.mpi.getCurrentPitchStepsAdjustment();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.getCurrentPosition()
- * Accurate only to frame size of encoded data (26 ms for MP3s)
- *
- * @return Current position (in milliseconds)
- */
- public int getCurrentPosition() {
- lock.lock();
- try {
- return (this.lastKnownPosition = this.mpi.getCurrentPosition());
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Returns the current speed multiplier. Defaults to 1.0 (normal speed)
- *
- * @return The current speed multiplier
- */
- public float getCurrentSpeedMultiplier() {
- lock.lock();
- try {
- return this.mpi.getCurrentSpeedMultiplier();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.getDuration()
- *
- * @return Length of the track (in milliseconds)
- */
- public int getDuration() {
- lock.lock();
- try {
- return this.mpi.getDuration();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Get the maximum value that can be passed to setPlaybackSpeed
- *
- * @return The maximum speed multiplier
- */
- public float getMaxSpeedMultiplier() {
- lock.lock();
- try {
- return this.mpi.getMaxSpeedMultiplier();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Get the minimum value that can be passed to setPlaybackSpeed
- *
- * @return The minimum speed multiplier
- */
- public float getMinSpeedMultiplier() {
- lock.lock();
- try {
- return this.mpi.getMinSpeedMultiplier();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Gets the version code of the backing service
- * @return -1 if ServiceBackedMediaPlayer is not used, 0 if the service is not
- * connected, otherwise the version code retrieved from the service
- */
- public int getServiceVersionCode() {
- lock.lock();
- try {
- if (this.mpi instanceof ServiceBackedMediaPlayer) {
- return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionCode();
- }
- else {
- return -1;
- }
- }
- finally {
- lock.unlock();
- }
- }
-
- /**
- * Gets the version name of the backing service
- * @return null if ServiceBackedMediaPlayer is not used, empty string if
- * the service is not connected, otherwise the version name retrieved from
- * the service
- */
- public String getServiceVersionName() {
- lock.lock();
- try {
- if (this.mpi instanceof ServiceBackedMediaPlayer) {
- return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionName();
- }
- else {
- return null;
- }
- }
- finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.isLooping()
- *
- * @return True if the track is looping
- */
- public boolean isLooping() {
- lock.lock();
- try {
- return this.mpi.isLooping();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.isPlaying()
- *
- * @return True if the track is playing
- */
- public boolean isPlaying() {
- lock.lock();
- try {
- return this.mpi.isPlaying();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Returns true if this MediaPlayer has access to the Presto
- * library
- *
- * @return True if the Presto library is installed
- */
- public boolean isPrestoLibraryInstalled() {
- if ((this.mpi == null) || (this.mpi.mContext == null)) {
- return false;
- }
- return isPrestoLibraryInstalled(this.mpi.mContext);
- }
-
- /**
- * Open the Android Market page in the same context as this MediaPlayer
- */
- public void openPrestoMarketIntent() {
- if ((this.mpi != null) && (this.mpi.mContext != null)) {
- openPrestoMarketIntent(this.mpi.mContext);
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.pause() Pauses the
- * track
- */
- public void pause() {
- lock.lock();
- try {
- if (invalidServiceConnectionConfiguration()) {
- setupMpi(this.mpi.mContext);
- }
- this.state = State.PAUSED;
- this.mpi.pause();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.prepare() Prepares the
- * track. This or prepareAsync must be called before start()
- */
- public void prepare() throws IllegalStateException, IOException {
- lock.lock();
- try {
- Log.d(MP_TAG, "prepare() 746 using " + ((this.mpi == null) ? "null (this shouldn't happen)" : this.mpi.getClass().toString()) + " state " + this.state.toString());
- Log.d(MP_TAG, "onPreparedListener is: " + ((this.onPreparedListener == null) ? "null" : "non-null"));
- Log.d(MP_TAG, "preparedListener is: " + ((this.preparedListener == null) ? "null" : "non-null"));
- if (invalidServiceConnectionConfiguration()) {
- setupMpi(this.mpi.mContext);
- }
- this.mpi.prepare();
- this.state = State.PREPARED;
- Log.d(MP_TAG, "prepare() finished 778");
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.prepareAsync()
- * Prepares the track. This or prepare must be called before start()
- */
- public void prepareAsync() {
- lock.lock();
- try {
- Log.d(MP_TAG, "prepareAsync() 779");
- if (invalidServiceConnectionConfiguration()) {
- setupMpi(this.mpi.mContext);
- }
- this.state = State.PREPARING;
- this.mpi.prepareAsync();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.release() Releases the
- * underlying resources used by the media player.
- */
- public void release() {
- lock.lock();
- try {
- Log.d(MP_TAG, "Releasing MediaPlayer 791");
-
- this.state = State.END;
- if (this.amp != null) {
- this.amp.release();
- }
- if (this.sbmp != null) {
- this.sbmp.release();
- }
-
- this.onBufferingUpdateListener = null;
- this.onCompletionListener = null;
- this.onErrorListener = null;
- this.onInfoListener = null;
- this.preparedListener = null;
- this.onPitchAdjustmentAvailableChangedListener = null;
- this.pitchAdjustmentAvailableChangedListener = null;
- Log.d(MP_TAG, "Setting onSeekCompleteListener to null 871");
- this.onSeekCompleteListener = null;
- this.onSpeedAdjustmentAvailableChangedListener = null;
- this.speedAdjustmentAvailableChangedListener = null;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.reset() Resets the
- * track to idle state
- */
- public void reset() {
- lock.lock();
- try {
- this.state = State.IDLE;
- this.stringDataSource = null;
- this.uriDataSource = null;
- this.mpi.reset();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.seekTo(int msec) Seeks
- * to msec in the track
- */
- public void seekTo(int msec) throws IllegalStateException {
- lock.lock();
- try {
- this.mpi.seekTo(msec);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setAudioStreamType(int
- * streamtype) Sets the audio stream type.
- */
- public void setAudioStreamType(int streamtype) {
- lock.lock();
- try {
- this.mAudioStreamType = streamtype;
- this.mpi.setAudioStreamType(streamtype);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setDataSource(Context
- * context, Uri uri) Sets uri as data source in the context given
- */
- public void setDataSource(Context context, Uri uri)
- throws IllegalArgumentException, IllegalStateException, IOException {
- lock.lock();
- try {
- Log.d(MP_TAG, "In setDataSource(context, " + uri.toString() + "), using " + this.mpi.getClass().toString());
- if (invalidServiceConnectionConfiguration()) {
- setupMpi(this.mpi.mContext);
- }
- this.state = State.INITIALIZED;
- this.stringDataSource = null;
- this.uriDataSource = uri;
- this.mpi.setDataSource(context, uri);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setDataSource(String
- * path) Sets the data source of the track to a file given.
- */
- public void setDataSource(String path) throws IllegalArgumentException,
- IllegalStateException, IOException {
- lock.lock();
- try {
- Log.d(MP_TAG, "In setDataSource(context, " + path + ")");
- if (invalidServiceConnectionConfiguration()) {
- setupMpi(this.mpi.mContext);
- }
- this.state = State.INITIALIZED;
- this.stringDataSource = path;
- this.uriDataSource = null;
- this.mpi.setDataSource(path);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Sets whether to use speed adjustment or not. Speed adjustment on is more
- * computation-intensive than with it off.
- *
- * @param enableSpeedAdjustment
- * Whether speed adjustment should be supported.
- */
- public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) {
- lock.lock();
- try {
- this.enableSpeedAdjustment = enableSpeedAdjustment;
- this.mpi.setEnableSpeedAdjustment(enableSpeedAdjustment);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setLooping(boolean
- * loop) Sets the track to loop infinitely if loop is true, play once if
- * loop is false
- */
- public void setLooping(boolean loop) {
- lock.lock();
- try {
- this.mIsLooping = loop;
- this.mpi.setLooping(loop);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Sets the number of steps (in a musical scale) by which playback is
- * currently shifted. When greater than zero, pitch is shifted up. When less
- * than zero, pitch is shifted down.
- *
- * @param pitchSteps
- * The number of steps by which to shift playback
- */
- public void setPitchStepsAdjustment(float pitchSteps) {
- lock.lock();
- try {
- this.mPitchStepsAdjustment = pitchSteps;
- this.mpi.setPitchStepsAdjustment(pitchSteps);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Set the algorithm to use for changing the speed and pitch of audio
- * See SpeedAdjustmentAlgorithm constants for more details
- * @param algorithm The algorithm to use.
- */
- public void setSpeedAdjustmentAlgorithm(int algorithm) {
- lock.lock();
- try {
- this.speedAdjustmentAlgorithm = algorithm;
- if (this.mpi != null) {
- this.mpi.setSpeedAdjustmentAlgorithm(algorithm);
- }
- }
- finally {
- lock.unlock();
- }
- }
-
- private static float getPitchStepsAdjustment(float pitch) {
- return (float) (Math.log(pitch) / (2 * Math.log(PITCH_STEP_CONSTANT)));
- }
-
- /**
- * Sets the percentage by which pitch is currently shifted. When greater
- * than zero, pitch is shifted up. When less than zero, pitch is shifted
- * down
- *
- * @param f
- * The percentage to shift pitch
- */
- public void setPlaybackPitch(float pitch) {
- lock.lock();
- try {
- this.mPitchStepsAdjustment = getPitchStepsAdjustment(pitch);
- this.mpi.setPlaybackPitch(pitch);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so on.
- * Speed should never be set to 0 or below.
- *
- * @param f
- * The speed multiplier to use for further playback
- */
- public void setPlaybackSpeed(float f) {
- lock.lock();
- try {
- this.mSpeedMultiplier = f;
- this.mpi.setPlaybackSpeed(f);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Sets whether to use speed adjustment or not. Speed adjustment on is more
- * computation-intensive than with it off.
- *
- * @param enableSpeedAdjustment
- * Whether speed adjustment should be supported.
- */
- public void setUseService(boolean useService) {
- lock.lock();
- try {
- this.useService = useService;
- setupMpi(this.mpi.mContext);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setVolume(float
- * leftVolume, float rightVolume) Sets the stereo volume
- */
- public void setVolume(float leftVolume, float rightVolume) {
- lock.lock();
- try {
- this.mLeftVolume = leftVolume;
- this.mRightVolume = rightVolume;
- this.mpi.setVolume(leftVolume, rightVolume);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setWakeMode(Context
- * context, int mode) Acquires a wake lock in the context given. You must
- * request the appropriate permissions in your AndroidManifest.xml file.
- */
- public void setWakeMode(Context context, int mode) {
- lock.lock();
- try {
- this.mWakeMode = mode;
- this.mpi.setWakeMode(context, mode);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to
- * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener
- * listener) Sets a listener to be used when a track completes playing.
- */
- public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) {
- lock.lock();
- try {
- this.onBufferingUpdateListener = listener;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to
- * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener
- * listener) Sets a listener to be used when a track completes playing.
- */
- public void setOnCompletionListener(OnCompletionListener listener) {
- lock.lock();
- try {
- this.onCompletionListener = listener;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to
- * android.media.MediaPlayer.setOnErrorListener(OnErrorListener listener)
- * Sets a listener to be used when a track encounters an error.
- */
- public void setOnErrorListener(OnErrorListener listener) {
- lock.lock();
- try {
- this.onErrorListener = listener;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to
- * android.media.MediaPlayer.setOnInfoListener(OnInfoListener listener) Sets
- * a listener to be used when a track has info.
- */
- public void setOnInfoListener(OnInfoListener listener) {
- lock.lock();
- try {
- this.onInfoListener = listener;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Sets a listener that will fire when pitch adjustment becomes available or
- * stops being available
- */
- public void setOnPitchAdjustmentAvailableChangedListener(
- OnPitchAdjustmentAvailableChangedListener listener) {
- lock.lock();
- try {
- this.pitchAdjustmentAvailableChangedListener = listener;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to
- * android.media.MediaPlayer.setOnPreparedListener(OnPreparedListener
- * listener) Sets a listener to be used when a track finishes preparing.
- */
- public void setOnPreparedListener(OnPreparedListener listener) {
- lock.lock();
- Log.d(MP_TAG, " ++++++++++++++++++++++++++++++++++++++++++++ setOnPreparedListener");
- try {
- this.preparedListener = listener;
- // For this one, we do not explicitly set the MediaPlayer or the
- // Service listener. This is because in addition to calling the
- // listener provided by the client, it's necessary to change
- // state to PREPARED. See prepareAsync for implementation details
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to
- * android.media.MediaPlayer.setOnSeekCompleteListener
- * (OnSeekCompleteListener listener) Sets a listener to be used when a track
- * finishes seeking.
- */
- public void setOnSeekCompleteListener(OnSeekCompleteListener listener) {
- lock.lock();
- try {
- this.onSeekCompleteListener = listener;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Sets a listener that will fire when speed adjustment becomes available or
- * stops being available
- */
- public void setOnSpeedAdjustmentAvailableChangedListener(
- OnSpeedAdjustmentAvailableChangedListener listener) {
- lock.lock();
- try {
- this.speedAdjustmentAvailableChangedListener = listener;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.start() Starts a track
- * playing
- */
- public void start() {
- lock.lock();
- try {
- Log.d(MP_TAG, "start() 1149");
- if (invalidServiceConnectionConfiguration()) {
- setupMpi(this.mpi.mContext);
- }
- this.state = State.STARTED;
- Log.d(MP_TAG, "start() 1154");
- this.mpi.start();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.stop() Stops a track
- * playing and resets its position to the start.
- */
- public void stop() {
- lock.lock();
- try {
- if (invalidServiceConnectionConfiguration()) {
- setupMpi(this.mpi.mContext);
- }
- this.state = State.STOPPED;
- this.mpi.stop();
- } finally {
- lock.unlock();
- }
- }
-} \ No newline at end of file
diff --git a/src/com/aocate/media/ServiceBackedMediaPlayer.java b/src/com/aocate/media/ServiceBackedMediaPlayer.java
deleted file mode 100644
index 8d08867ef..000000000
--- a/src/com/aocate/media/ServiceBackedMediaPlayer.java
+++ /dev/null
@@ -1,1201 +0,0 @@
-// 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.
-//
-// -----------------------------------------------------------------------
-// Compared to the original version, this class been slightly modified so
-// that any acquired WakeLocks are only held while the MediaPlayer is
-// playing (see the stayAwake method for more details).
-
-
-package com.aocate.media;
-
-import java.io.IOException;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.media.AudioManager;
-import android.net.Uri;
-import android.os.IBinder;
-import android.os.PowerManager;
-import android.os.RemoteException;
-import android.os.PowerManager.WakeLock;
-import android.util.Log;
-
-import com.aocate.media.MediaPlayer.State;
-import com.aocate.presto.service.IDeathCallback_0_8;
-import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8;
-import com.aocate.presto.service.IOnCompletionListenerCallback_0_8;
-import com.aocate.presto.service.IOnErrorListenerCallback_0_8;
-import com.aocate.presto.service.IOnInfoListenerCallback_0_8;
-import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8;
-import com.aocate.presto.service.IOnPreparedListenerCallback_0_8;
-import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8;
-import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8;
-import com.aocate.presto.service.IPlayMedia_0_8;
-
-import de.danoeh.antennapod.BuildConfig;
-
-/**
- * Class for connecting to remote speed-altering, media playing Service
- * Note that there is unusually high coupling between MediaPlayer and this
- * class. This is an unfortunate compromise, since the alternative was to
- * track state in two different places in this code (plus the internal state
- * of the remote media player).
- * @author aocate
- *
- */
-public class ServiceBackedMediaPlayer extends MediaPlayerImpl {
- static final String INTENT_NAME = "com.aocate.intent.PLAY_AUDIO_ADJUST_SPEED_0_8";
-
- private static final String SBMP_TAG = "AocateServiceBackedMediaPlayer";
-
- private ServiceConnection mPlayMediaServiceConnection = null;
- protected IPlayMedia_0_8 pmInterface = null;
- private Intent playMediaServiceIntent = null;
- // In some cases, we're going to have to replace the
- // android.media.MediaPlayer on the fly, and we don't want to touch the
- // wrong media player.
-
- private long sessionId = 0;
- private boolean isErroring = false;
- private int mAudioStreamType = AudioManager.STREAM_MUSIC;
-
- private WakeLock mWakeLock = null;
-
- // So here's the major problem
- // Sometimes the service won't exist or won't be connected,
- // so start with an android.media.MediaPlayer, and when
- // the service is connected, use that from then on
- public ServiceBackedMediaPlayer(MediaPlayer owningMediaPlayer, final Context context, final ServiceConnection serviceConnection) {
- super(owningMediaPlayer, context);
- Log.d(SBMP_TAG, "Instantiating ServiceBackedMediaPlayer 87");
- this.playMediaServiceIntent =
- new Intent(INTENT_NAME);
- this.mPlayMediaServiceConnection = new ServiceConnection() {
- public void onServiceConnected(ComponentName name, IBinder service) {
- IPlayMedia_0_8 tmpPlayMediaInterface = IPlayMedia_0_8.Stub.asInterface((IBinder) service);
-
- Log.d(SBMP_TAG, "Setting up pmInterface 94");
- if (ServiceBackedMediaPlayer.this.sessionId == 0) {
- try {
- // The IDeathCallback isn't a conventional callback.
- // It exists so that if the client ceases to exist,
- // the Service becomes aware of that and can shut
- // down whatever it needs to shut down
- ServiceBackedMediaPlayer.this.sessionId = tmpPlayMediaInterface.startSession(new IDeathCallback_0_8.Stub() {
- });
- // This is really bad if this fails
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- Log.d(SBMP_TAG, "Assigning pmInterface");
-
- ServiceBackedMediaPlayer.this.setOnBufferingUpdateCallback(tmpPlayMediaInterface);
- ServiceBackedMediaPlayer.this.setOnCompletionCallback(tmpPlayMediaInterface);
- ServiceBackedMediaPlayer.this.setOnErrorCallback(tmpPlayMediaInterface);
- ServiceBackedMediaPlayer.this.setOnInfoCallback(tmpPlayMediaInterface);
- ServiceBackedMediaPlayer.this.setOnPitchAdjustmentAvailableChangedListener(tmpPlayMediaInterface);
- ServiceBackedMediaPlayer.this.setOnPreparedCallback(tmpPlayMediaInterface);
- ServiceBackedMediaPlayer.this.setOnSeekCompleteCallback(tmpPlayMediaInterface);
- ServiceBackedMediaPlayer.this.setOnSpeedAdjustmentAvailableChangedCallback(tmpPlayMediaInterface);
-
- // In order to avoid race conditions from the sessionId or listener not being assigned
- pmInterface = tmpPlayMediaInterface;
-
- Log.d(SBMP_TAG, "Invoking onServiceConnected");
- serviceConnection.onServiceConnected(name, service);
- }
-
- public void onServiceDisconnected(ComponentName name) {
- Log.d(SBMP_TAG, "onServiceDisconnected 114");
-
- pmInterface = null;
-
- sessionId = 0;
-
- serviceConnection.onServiceDisconnected(name);
- }
- };
-
- Log.d(SBMP_TAG, "Connecting PlayMediaService 124");
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private boolean ConnectPlayMediaService() {
- Log.d(SBMP_TAG, "ConnectPlayMediaService()");
-
- if (MediaPlayer.isIntentAvailable(mContext, INTENT_NAME)) {
- Log.d(SBMP_TAG, INTENT_NAME + " is available");
- if (pmInterface == null) {
- try {
- Log.d(SBMP_TAG, "Binding service");
- return mContext.bindService(playMediaServiceIntent, mPlayMediaServiceConnection, Context.BIND_AUTO_CREATE);
- } catch (Exception e) {
- return false;
- }
- } else {
- Log.d(SBMP_TAG, "Service already bound");
- return true;
- }
- }
- else {
- Log.d(SBMP_TAG, INTENT_NAME + " is not available");
- return false;
- }
- }
-
- /**
- * Returns true if pitch can be changed at this moment
- * @return True if pitch can be changed
- */
- @Override
- public boolean canSetPitch() {
- Log.d(SBMP_TAG, "canSetPitch() 155");
-
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set pitch if the service isn't connected
- try {
- return pmInterface.canSetPitch(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- return false;
- }
-
- /**
- * Returns true if speed can be changed at this moment
- * @return True if speed can be changed
- */
- @Override
- public boolean canSetSpeed() {
- Log.d(SBMP_TAG, "canSetSpeed() 180");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the service isn't connected
- try {
- return pmInterface.canSetSpeed(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- return false;
- }
-
- void error(int what, int extra) {
- owningMediaPlayer.lock.lock();
- Log.e(SBMP_TAG, "error(" + what + ", " + extra + ")");
- stayAwake(false);
- try {
- if (!this.isErroring) {
- this.isErroring = true;
- owningMediaPlayer.state = State.ERROR;
- if (owningMediaPlayer.onErrorListener != null) {
- if (owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra)) {
- return;
- }
- }
- if (owningMediaPlayer.onCompletionListener != null) {
- owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer);
- }
- }
- }
- finally {
- this.isErroring = false;
- owningMediaPlayer.lock.unlock();
- }
- }
-
- protected void finalize() throws Throwable {
- owningMediaPlayer.lock.lock();
- try {
- Log.d(SBMP_TAG, "finalize() 224");
- this.release();
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
-
- /**
- * Returns the number of steps (in a musical scale) by which playback is
- * currently shifted. When greater than zero, pitch is shifted up.
- * When less than zero, pitch is shifted down.
- * @return The number of steps pitch is currently shifted by
- */
- @Override
- public float getCurrentPitchStepsAdjustment() {
- Log.d(SBMP_TAG, "getCurrentPitchStepsAdjustment() 240");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set pitch if the service isn't connected
- try {
- return pmInterface.getCurrentPitchStepsAdjustment(
- ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- return 0f;
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.getCurrentPosition()
- * @return Current position (in milliseconds)
- */
- @Override
- public int getCurrentPosition() {
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- return pmInterface.getCurrentPosition(
- ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- return 0;
- }
-
- /**
- * Returns the current speed multiplier. Defaults to 1.0 (normal speed)
- * @return The current speed multiplier
- */
- @Override
- public float getCurrentSpeedMultiplier() {
- Log.d(SBMP_TAG, "getCurrentSpeedMultiplier() 286");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the service isn't connected
- try {
- return pmInterface.getCurrentSpeedMultiplier(
- ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- return 1;
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.getDuration()
- * @return Length of the track (in milliseconds)
- */
- @Override
- public int getDuration() {
- Log.d(SBMP_TAG, "getDuration() 311");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- return pmInterface.getDuration(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- return 0;
- }
-
- /**
- * Get the maximum value that can be passed to setPlaybackSpeed
- * @return The maximum speed multiplier
- */
- @Override
- public float getMaxSpeedMultiplier() {
- Log.d(SBMP_TAG, "getMaxSpeedMultiplier() 332");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the Service isn't connected
- try {
- return pmInterface.getMaxSpeedMultiplier(
- ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- return 1f;
- }
-
- /**
- * Get the minimum value that can be passed to setPlaybackSpeed
- * @return The minimum speed multiplier
- */
- @Override
- public float getMinSpeedMultiplier() {
- Log.d(SBMP_TAG, "getMinSpeedMultiplier() 357");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the Service isn't connected
- try {
- return pmInterface.getMinSpeedMultiplier(
- ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- return 1f;
- }
-
- public int getServiceVersionCode() {
- Log.d(SBMP_TAG, "getVersionCode");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- return pmInterface.getVersionCode();
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- return 0;
- }
-
- public String getServiceVersionName() {
- Log.d(SBMP_TAG, "getVersionName");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- return pmInterface.getVersionName();
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- return "";
- }
-
- public boolean isConnected() {
- return (pmInterface != null);
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.isLooping()
- * @return True if the track is looping
- */
- @Override
- public boolean isLooping() {
- Log.d(SBMP_TAG, "isLooping() 382");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- return pmInterface.isLooping(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- return false;
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.isPlaying()
- * @return True if the track is playing
- */
- @Override
- public boolean isPlaying() {
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- try {
- return pmInterface.isPlaying(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- return false;
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.pause()
- * Pauses the track
- */
- @Override
- public void pause() {
- Log.d(SBMP_TAG, "pause() 424");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.pause(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- stayAwake(false);
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.prepare()
- * Prepares the track. This or prepareAsync must be called before start()
- */
- @Override
- public void prepare() throws IllegalStateException, IOException {
- Log.d(SBMP_TAG, "prepare() 444");
- Log.d(SBMP_TAG, "onPreparedCallback is: " + ((this.mOnPreparedCallback == null) ? "null" : "non-null"));
- if (pmInterface == null) {
- Log.d(SBMP_TAG, "prepare: pmInterface is null");
- if (!ConnectPlayMediaService()) {
- Log.d(SBMP_TAG, "prepare: Failed to connect play media service");
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- Log.d(SBMP_TAG, "prepare: pmInterface isn't null");
- try {
- Log.d(SBMP_TAG, "prepare: Remote invoke pmInterface.prepare(" + ServiceBackedMediaPlayer.this.sessionId + ")");
- pmInterface.prepare(ServiceBackedMediaPlayer.this.sessionId);
- Log.d(SBMP_TAG, "prepare: prepared");
- } catch (RemoteException e) {
- Log.d(SBMP_TAG, "prepare: RemoteException");
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- Log.d(SBMP_TAG, "Done with prepare()");
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.prepareAsync()
- * Prepares the track. This or prepare must be called before start()
- */
- @Override
- public void prepareAsync() {
- Log.d(SBMP_TAG, "prepareAsync() 469");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.prepareAsync(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.release()
- * Releases the underlying resources used by the media player.
- */
- @Override
- public void release() {
- Log.d(SBMP_TAG, "release() 492");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- Log.d(SBMP_TAG, "release() 500");
- try {
- pmInterface.release(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- mContext.unbindService(this.mPlayMediaServiceConnection);
- // Don't try to keep awake (if we were)
- this.setWakeMode(mContext, 0);
- pmInterface = null;
- this.sessionId = 0;
- }
-
- if ((this.mWakeLock != null) && this.mWakeLock.isHeld()) {
- Log.d(SBMP_TAG, "Releasing wakelock");
- this.mWakeLock.release();
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.reset()
- * Resets the track to idle state
- */
- @Override
- public void reset() {
- Log.d(SBMP_TAG, "reset() 523");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.reset(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- stayAwake(false);
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.seekTo(int msec)
- * Seeks to msec in the track
- */
- @Override
- public void seekTo(int msec) throws IllegalStateException {
- Log.d(SBMP_TAG, "seekTo(" + msec + ")");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.seekTo(ServiceBackedMediaPlayer.this.sessionId, msec);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setAudioStreamType(int streamtype)
- * Sets the audio stream type.
- */
- @Override
- public void setAudioStreamType(int streamtype) {
- Log.d(SBMP_TAG, "setAudioStreamType(" + streamtype + ")");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.setAudioStreamType(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mAudioStreamType);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
-
- /**
- * Functions identically to android.media.MediaPlayer.setDataSource(Context context, Uri uri)
- * Sets uri as data source in the context given
- */
- @Override
- public void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException {
- Log.d(SBMP_TAG, "setDataSource(context, uri)");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.setDataSourceUri(
- ServiceBackedMediaPlayer.this.sessionId,
- uri);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setDataSource(String path)
- * Sets the data source of the track to a file given.
- */
- @Override
- public void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException {
- Log.d(SBMP_TAG, "setDataSource(path)");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface == null) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- else {
- try {
- pmInterface.setDataSourceString(
- ServiceBackedMediaPlayer.this.sessionId,
- path);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- }
-
- /**
- * Sets whether to use speed adjustment or not. Speed adjustment on is
- * more computation-intensive than with it off.
- * @param enableSpeedAdjustment Whether speed adjustment should be supported.
- */
- @Override
- public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) {
- // TODO: This has no business being here, I think
- owningMediaPlayer.lock.lock();
- Log.d(SBMP_TAG, "setEnableSpeedAdjustment(enableSpeedAdjustment)");
- try {
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the Service isn't connected
- try {
- pmInterface.setEnableSpeedAdjustment(
- ServiceBackedMediaPlayer.this.sessionId,
- enableSpeedAdjustment);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
-
-
- /**
- * Functions identically to android.media.MediaPlayer.setLooping(boolean loop)
- * Sets the track to loop infinitely if loop is true, play once if loop is false
- */
- @Override
- public void setLooping(boolean loop) {
- Log.d(SBMP_TAG, "setLooping(" + loop + ")");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.setLooping(ServiceBackedMediaPlayer.this.sessionId, loop);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- /**
- * Sets the number of steps (in a musical scale) by which playback is
- * currently shifted. When greater than zero, pitch is shifted up.
- * When less than zero, pitch is shifted down.
- *
- * @param pitchSteps The number of steps by which to shift playback
- */
- @Override
- public void setPitchStepsAdjustment(float pitchSteps) {
- Log.d(SBMP_TAG, "setPitchStepsAdjustment(" + pitchSteps + ")");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the Service isn't connected
- try {
- pmInterface.setPitchStepsAdjustment(
- ServiceBackedMediaPlayer.this.sessionId,
- pitchSteps);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- }
-
- /**
- * Sets the percentage by which pitch is currently shifted. When
- * greater than zero, pitch is shifted up. When less than zero, pitch
- * is shifted down
- * @param f The percentage to shift pitch
- */
- @Override
- public void setPlaybackPitch(float f) {
- Log.d(SBMP_TAG, "setPlaybackPitch(" + f + ")");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the Service isn't connected
- try {
- pmInterface.setPlaybackPitch(
- ServiceBackedMediaPlayer.this.sessionId,
- f);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- }
-
- /**
- * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so
- * on. Speed should never be set to 0 or below.
- * @param f The speed multiplier to use for further playback
- */
- @Override
- public void setPlaybackSpeed(float f) {
- Log.d(SBMP_TAG, "setPlaybackSpeed(" + f + ")");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- if (pmInterface != null) {
- // Can't set speed if the Service isn't connected
- try {
- pmInterface.setPlaybackSpeed(
- ServiceBackedMediaPlayer.this.sessionId,
- f);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- }
-
- @Override
- public void setSpeedAdjustmentAlgorithm(int algorithm) {
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.setSpeedAdjustmentAlgorithm(
- ServiceBackedMediaPlayer.this.sessionId,
- algorithm);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setVolume(float leftVolume, float rightVolume)
- * Sets the stereo volume
- */
- @Override
- public void setVolume(float leftVolume, float rightVolume) {
- Log.d(SBMP_TAG, "setVolume(" + leftVolume + ", " + rightVolume + ")");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.setVolume(
- ServiceBackedMediaPlayer.this.sessionId,
- leftVolume,
- rightVolume);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.setWakeMode(Context context, int mode)
- * Acquires a wake lock in the context given. You must request the appropriate permissions
- * in your AndroidManifest.xml file.
- */
- @Override
- // This does not just call .setWakeMode() in the Service because doing so
- // would add a permission requirement to the Service. Do it here, and it's
- // the client app's responsibility to request that permission
- public void setWakeMode(Context context, int mode) {
- Log.d(SBMP_TAG, "setWakeMode(context, " + mode + ")");
- if ((this.mWakeLock != null)
- && (this.mWakeLock.isHeld())) {
- this.mWakeLock.release();
- }
- if (mode != 0) {
- if (this.mWakeLock == null) {
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- // Since mode can't be changed on the fly, we have to allocate a new one
- this.mWakeLock = pm.newWakeLock(mode, this.getClass().getName());
- this.mWakeLock.setReferenceCounted(false);
- }
-
- this.mWakeLock.acquire();
- }
- }
-
- /**
- * Changes the state of the WakeLock if it has been acquired.
- * If no WakeLock has been acquired with setWakeMode, this method does nothing.
- * */
- private void stayAwake(boolean awake) {
- if (BuildConfig.DEBUG) Log.d(SBMP_TAG, "stayAwake(" + awake + ")");
- if (mWakeLock != null) {
- if (awake && !mWakeLock.isHeld()) {
- mWakeLock.acquire();
- } else if (!awake && mWakeLock.isHeld()) {
- mWakeLock.release();
- }
- }
- }
-
- private IOnBufferingUpdateListenerCallback_0_8.Stub mOnBufferingUpdateCallback = null;
- private void setOnBufferingUpdateCallback(IPlayMedia_0_8 iface) {
- try {
- if (this.mOnBufferingUpdateCallback == null) {
- mOnBufferingUpdateCallback = new IOnBufferingUpdateListenerCallback_0_8.Stub() {
- public void onBufferingUpdate(int percent)
- throws RemoteException {
- owningMediaPlayer.lock.lock();
- try {
- if ((owningMediaPlayer.onBufferingUpdateListener != null)
- && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) {
- owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent);
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
- };
- }
- iface.registerOnBufferingUpdateCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- mOnBufferingUpdateCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private IOnCompletionListenerCallback_0_8.Stub mOnCompletionCallback = null;
- private void setOnCompletionCallback(IPlayMedia_0_8 iface) {
- try {
- if (this.mOnCompletionCallback == null) {
- this.mOnCompletionCallback = new IOnCompletionListenerCallback_0_8.Stub() {
- public void onCompletion() throws RemoteException {
- owningMediaPlayer.lock.lock();
- Log.d(SBMP_TAG, "onCompletionListener being called");
- stayAwake(false);
- try {
- if (owningMediaPlayer.onCompletionListener != null) {
- owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer);
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
- };
- }
- iface.registerOnCompletionCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mOnCompletionCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private IOnErrorListenerCallback_0_8.Stub mOnErrorCallback = null;
- private void setOnErrorCallback(IPlayMedia_0_8 iface) {
- try {
- if (this.mOnErrorCallback == null) {
- this.mOnErrorCallback = new IOnErrorListenerCallback_0_8.Stub() {
- public boolean onError(int what, int extra) throws RemoteException {
- owningMediaPlayer.lock.lock();
- stayAwake(false);
- try {
- if (owningMediaPlayer.onErrorListener != null) {
- return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra);
- }
- return false;
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
- };
- }
- iface.registerOnErrorCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mOnErrorCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private IOnInfoListenerCallback_0_8.Stub mOnInfoCallback = null;
- private void setOnInfoCallback(IPlayMedia_0_8 iface) {
- try {
- if (this.mOnInfoCallback == null) {
- this.mOnInfoCallback = new IOnInfoListenerCallback_0_8.Stub() {
- public boolean onInfo(int what, int extra) throws RemoteException {
- owningMediaPlayer.lock.lock();
- try {
- if ((owningMediaPlayer.onInfoListener != null)
- && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) {
- return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra);
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- return false;
- }
- };
- }
- iface.registerOnInfoCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mOnInfoCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnPitchAdjustmentAvailableChangedCallback = null;
- private void setOnPitchAdjustmentAvailableChangedListener(IPlayMedia_0_8 iface) {
- try {
- if (this.mOnPitchAdjustmentAvailableChangedCallback == null) {
- this.mOnPitchAdjustmentAvailableChangedCallback = new IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub() {
- public void onPitchAdjustmentAvailableChanged(
- boolean pitchAdjustmentAvailable)
- throws RemoteException {
- owningMediaPlayer.lock.lock();
- try {
- if (owningMediaPlayer.onPitchAdjustmentAvailableChangedListener != null) {
- owningMediaPlayer.onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged(owningMediaPlayer, pitchAdjustmentAvailable);
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
- };
- }
- iface.registerOnPitchAdjustmentAvailableChangedCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mOnPitchAdjustmentAvailableChangedCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private IOnPreparedListenerCallback_0_8.Stub mOnPreparedCallback = null;
- private void setOnPreparedCallback(IPlayMedia_0_8 iface) {
- try {
- if (this.mOnPreparedCallback == null) {
- this.mOnPreparedCallback = new IOnPreparedListenerCallback_0_8.Stub() {
- public void onPrepared() throws RemoteException {
- owningMediaPlayer.lock.lock();
- Log.d(SBMP_TAG, "setOnPreparedCallback.mOnPreparedCallback.onPrepared 1050");
- try {
- Log.d(SBMP_TAG, "owningMediaPlayer.onPreparedListener is " + ((owningMediaPlayer.onPreparedListener == null) ? "null" : "non-null"));
- Log.d(SBMP_TAG, "owningMediaPlayer.mpi is " + ((owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this) ? "this" : "not this"));
- ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.lock();
- try {
- if (ServiceBackedMediaPlayer.this.muteOnPreparedCount > 0) {
- ServiceBackedMediaPlayer.this.muteOnPreparedCount--;
- }
- else {
- ServiceBackedMediaPlayer.this.muteOnPreparedCount = 0;
- if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) {
- owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer);
- }
- }
- }
- finally {
- ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.unlock();
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
- };
- }
- iface.registerOnPreparedCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mOnPreparedCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private IOnSeekCompleteListenerCallback_0_8.Stub mOnSeekCompleteCallback = null;
- private void setOnSeekCompleteCallback(IPlayMedia_0_8 iface) {
- try {
- if (this.mOnSeekCompleteCallback == null) {
- this.mOnSeekCompleteCallback = new IOnSeekCompleteListenerCallback_0_8.Stub() {
- public void onSeekComplete() throws RemoteException {
- Log.d(SBMP_TAG, "onSeekComplete() 941");
- owningMediaPlayer.lock.lock();
- try {
- if (ServiceBackedMediaPlayer.this.muteOnSeekCount > 0) {
- Log.d(SBMP_TAG, "The next " + ServiceBackedMediaPlayer.this.muteOnSeekCount + " seek events are muted (counting this one)");
- ServiceBackedMediaPlayer.this.muteOnSeekCount--;
- }
- else {
- ServiceBackedMediaPlayer.this.muteOnSeekCount = 0;
- Log.d(SBMP_TAG, "Attempting to invoke next seek event");
- if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) {
- Log.d(SBMP_TAG, "Invoking onSeekComplete");
- owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer);
- }
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
- };
- }
- iface.registerOnSeekCompleteCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mOnSeekCompleteCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- private IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnSpeedAdjustmentAvailableChangedCallback = null;
- private void setOnSpeedAdjustmentAvailableChangedCallback(IPlayMedia_0_8 iface) {
- try {
- Log.d(SBMP_TAG, "Setting the service of on speed adjustment available changed");
- if (this.mOnSpeedAdjustmentAvailableChangedCallback == null) {
- this.mOnSpeedAdjustmentAvailableChangedCallback = new IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub() {
- public void onSpeedAdjustmentAvailableChanged(
- boolean speedAdjustmentAvailable)
- throws RemoteException {
- owningMediaPlayer.lock.lock();
- try {
- if (owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener != null) {
- owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged(owningMediaPlayer, speedAdjustmentAvailable);
- }
- }
- finally {
- owningMediaPlayer.lock.unlock();
- }
- }
- };
- }
- iface.registerOnSpeedAdjustmentAvailableChangedCallback(
- ServiceBackedMediaPlayer.this.sessionId,
- this.mOnSpeedAdjustmentAvailableChangedCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.start()
- * Starts a track playing
- */
- @Override
- public void start() {
- Log.d(SBMP_TAG, "start()");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.start(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- stayAwake(true);
- }
-
- /**
- * Functions identically to android.media.MediaPlayer.stop()
- * Stops a track playing and resets its position to the start.
- */
- @Override
- public void stop() {
- Log.d(SBMP_TAG, "stop()");
- if (pmInterface == null) {
- if (!ConnectPlayMediaService()) {
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- }
- try {
- pmInterface.stop(ServiceBackedMediaPlayer.this.sessionId);
- } catch (RemoteException e) {
- e.printStackTrace();
- ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
- }
- stayAwake(false);
- }
-} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/PodcastApp.java b/src/de/danoeh/antennapod/PodcastApp.java
deleted file mode 100644
index 74628f3d6..000000000
--- a/src/de/danoeh/antennapod/PodcastApp.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package de.danoeh.antennapod;
-
-import android.app.Application;
-import android.content.res.Configuration;
-import android.util.Log;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.spa.SPAUtil;
-
-/** Main application class. */
-public class PodcastApp extends Application {
-
- private static final String TAG = "PodcastApp";
- public static final String EXPORT_DIR = "export/";
-
- 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/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
deleted file mode 100644
index 18d27ddda..000000000
--- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java
+++ /dev/null
@@ -1,746 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.dialog.VariableSpeedDialog;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.feed.SimpleChapter;
-import de.danoeh.antennapod.fragment.CoverFragment;
-import de.danoeh.antennapod.fragment.ItemDescriptionFragment;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.menuhandler.NavDrawerActivity;
-import de.danoeh.antennapod.util.playback.ExternalMedia;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java
deleted file mode 100644
index e8bc75293..000000000
--- a/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java
+++ /dev/null
@@ -1,248 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java b/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
deleted file mode 100644
index 06a11c775..000000000
--- a/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
+++ /dev/null
@@ -1,370 +0,0 @@
-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.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/src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java
deleted file mode 100644
index c5f25d813..000000000
--- a/src/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java
+++ /dev/null
@@ -1,110 +0,0 @@
-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.preferences.UserPreferences;
-import de.danoeh.antennapod.service.download.DownloadRequest;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/FeedInfoActivity.java b/src/de/danoeh/antennapod/activity/FeedInfoActivity.java
deleted file mode 100644
index 5cf187eb6..000000000
--- a/src/de/danoeh/antennapod/activity/FeedInfoActivity.java
+++ /dev/null
@@ -1,192 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.util.LangUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java b/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java
deleted file mode 100644
index 8dde14d3b..000000000
--- a/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java
+++ /dev/null
@@ -1,125 +0,0 @@
-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.preferences.UserPreferences;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java
deleted file mode 100644
index b7014dab2..000000000
--- a/src/de/danoeh/antennapod/activity/MainActivity.java
+++ /dev/null
@@ -1,432 +0,0 @@
-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.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.fragment.*;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.util.StorageUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
deleted file mode 100644
index 2e5372b60..000000000
--- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
+++ /dev/null
@@ -1,525 +0,0 @@
-package de.danoeh.antennapod.activity;
-
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.res.TypedArray;
-import android.graphics.PixelFormat;
-import android.media.AudioManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-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.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.util.ShareUtils;
-import de.danoeh.antennapod.util.StorageUtils;
-import de.danoeh.antennapod.util.playback.MediaPlayerError;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.util.playback.PlaybackController;
-import org.shredzone.flattr4j.model.User;
-
-/**
- * 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/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
deleted file mode 100644
index 2c6d75cd8..000000000
--- a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
+++ /dev/null
@@ -1,428 +0,0 @@
-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.feed.Feed;
-import de.danoeh.antennapod.feed.FeedPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.download.DownloadRequest;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.service.download.HttpDownloader;
-import de.danoeh.antennapod.syndication.handler.FeedHandler;
-import de.danoeh.antennapod.syndication.handler.FeedHandlerResult;
-import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException;
-import de.danoeh.antennapod.util.DownloadError;
-import de.danoeh.antennapod.util.FileNameGenerator;
-import de.danoeh.antennapod.util.StorageUtils;
-import de.danoeh.antennapod.util.URLChecker;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java b/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java
deleted file mode 100644
index e09941abf..000000000
--- a/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java
+++ /dev/null
@@ -1,134 +0,0 @@
-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.opml.OpmlElement;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java b/src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java
deleted file mode 100644
index d3fd3949c..000000000
--- a/src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java
+++ /dev/null
@@ -1,90 +0,0 @@
-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.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/src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java b/src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
deleted file mode 100644
index 16e663fac..000000000
--- a/src/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package de.danoeh.antennapod.activity;
-
-import android.app.AlertDialog;
-import android.os.Bundle;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
deleted file mode 100644
index 94f100321..000000000
--- a/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
+++ /dev/null
@@ -1,172 +0,0 @@
-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.preferences.UserPreferences;
-import de.danoeh.antennapod.util.LangUtils;
-import de.danoeh.antennapod.util.StorageUtils;
-
-import java.io.*;
-
-/**
- * Lets the user start the OPML-import process from a path
- */
-public class OpmlImportFromPathActivity extends OpmlImportBaseActivity {
- public static final String IMPORT_DIR = "import/";
- 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, 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/src/de/danoeh/antennapod/activity/OpmlImportHolder.java b/src/de/danoeh/antennapod/activity/OpmlImportHolder.java
deleted file mode 100644
index ec53ed7b6..000000000
--- a/src/de/danoeh/antennapod/activity/OpmlImportHolder.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.danoeh.antennapod.activity;
-
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java
deleted file mode 100644
index a21985bb8..000000000
--- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java
+++ /dev/null
@@ -1,532 +0,0 @@
-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.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.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
-import de.danoeh.antennapod.util.flattr.SimpleFlattrThing;
-
-import java.io.File;
-import java.util.ArrayList;
-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/src/de/danoeh/antennapod/activity/StorageErrorActivity.java b/src/de/danoeh/antennapod/activity/StorageErrorActivity.java
deleted file mode 100644
index d8a137eb9..000000000
--- a/src/de/danoeh/antennapod/activity/StorageErrorActivity.java
+++ /dev/null
@@ -1,75 +0,0 @@
-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.preferences.UserPreferences;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java
deleted file mode 100644
index 81661a288..000000000
--- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java
+++ /dev/null
@@ -1,359 +0,0 @@
-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.feed.MediaType;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.service.playback.PlayerStatus;
-import de.danoeh.antennapod.util.playback.ExternalMedia;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.view.AspectRatioVideoView;
-
-/**
- * Activity for playing 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/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java
deleted file mode 100644
index 6a60f65fe..000000000
--- a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java
+++ /dev/null
@@ -1,372 +0,0 @@
-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.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice;
-import de.danoeh.antennapod.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/ActionButtonCallback.java b/src/de/danoeh/antennapod/adapter/ActionButtonCallback.java
deleted file mode 100644
index 30ad2d03f..000000000
--- a/src/de/danoeh/antennapod/adapter/ActionButtonCallback.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package de.danoeh.antennapod.adapter;
-
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/src/de/danoeh/antennapod/adapter/ActionButtonUtils.java
deleted file mode 100644
index 1de071a73..000000000
--- a/src/de/danoeh/antennapod/adapter/ActionButtonUtils.java
+++ /dev/null
@@ -1,78 +0,0 @@
-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.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/AdapterUtils.java b/src/de/danoeh/antennapod/adapter/AdapterUtils.java
deleted file mode 100644
index f393fb7d7..000000000
--- a/src/de/danoeh/antennapod/adapter/AdapterUtils.java
+++ /dev/null
@@ -1,57 +0,0 @@
-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.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java
deleted file mode 100644
index c12de6ebd..000000000
--- a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java
+++ /dev/null
@@ -1,180 +0,0 @@
-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.feed.Chapter;
-import de.danoeh.antennapod.util.ChapterUtils;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
deleted file mode 100644
index 0c4cbe685..000000000
--- a/src/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
+++ /dev/null
@@ -1,57 +0,0 @@
-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.dialog.DownloadRequestErrorDialogCreator;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
deleted file mode 100644
index 2cc216227..000000000
--- a/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
+++ /dev/null
@@ -1,112 +0,0 @@
-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.feed.Feed;
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java
deleted file mode 100644
index ef5af67de..000000000
--- a/src/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java
+++ /dev/null
@@ -1,122 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java
deleted file mode 100644
index 658af9e4e..000000000
--- a/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java
+++ /dev/null
@@ -1,142 +0,0 @@
-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.service.download.DownloadRequest;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
deleted file mode 100644
index 3f666eb8b..000000000
--- a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
+++ /dev/null
@@ -1,306 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
deleted file mode 100644
index 357b5f8b4..000000000
--- a/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
+++ /dev/null
@@ -1,220 +0,0 @@
-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.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
deleted file mode 100644
index c2c2285ac..000000000
--- a/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
+++ /dev/null
@@ -1,55 +0,0 @@
-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.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/src/de/danoeh/antennapod/adapter/NavListAdapter.java b/src/de/danoeh/antennapod/adapter/NavListAdapter.java
deleted file mode 100644
index ef8e8ce07..000000000
--- a/src/de/danoeh/antennapod/adapter/NavListAdapter.java
+++ /dev/null
@@ -1,229 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java
deleted file mode 100644
index 8abe49133..000000000
--- a/src/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java
+++ /dev/null
@@ -1,170 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/QueueListAdapter.java b/src/de/danoeh/antennapod/adapter/QueueListAdapter.java
deleted file mode 100644
index ebe519592..000000000
--- a/src/de/danoeh/antennapod/adapter/QueueListAdapter.java
+++ /dev/null
@@ -1,127 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java b/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java
deleted file mode 100644
index 2314c2269..000000000
--- a/src/de/danoeh/antennapod/adapter/SearchlistAdapter.java
+++ /dev/null
@@ -1,110 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedComponent;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.SearchResult;
-
-/**
- * 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/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
deleted file mode 100644
index aeb1fc53a..000000000
--- a/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
+++ /dev/null
@@ -1,68 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/asynctask/DownloadObserver.java b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java
deleted file mode 100644
index 21ae5291e..000000000
--- a/src/de/danoeh/antennapod/asynctask/DownloadObserver.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package de.danoeh.antennapod.asynctask;
-
-import android.app.Activity;
-import android.content.*;
-import android.os.Handler;
-import android.os.IBinder;
-import android.util.Log;
-
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.service.download.DownloadService;
-import de.danoeh.antennapod.service.download.Downloader;
-
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Provides access to the DownloadService's list of items that are currently being downloaded.
- * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause()
- * should be called in the activity's onResume() and onPause() methods
- */
-public class DownloadObserver {
- private static final String TAG = "DownloadObserver";
-
- /**
- * Time period between update notifications.
- */
- public static final int WAITING_INTERVAL_MS = 3000;
-
- private volatile Activity activity;
- private final Handler handler;
- private final Callback callback;
-
- private DownloadService downloadService = null;
- private AtomicBoolean mIsBound = new AtomicBoolean(false);
-
- private Thread refresherThread;
- private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false);
-
-
- /**
- * Creates a new download observer.
- *
- * @param activity Used for registering receivers
- * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread.
- * @param callback Callback methods for posting content updates
- * @throws java.lang.IllegalArgumentException if one of the arguments is null.
- */
- public DownloadObserver(Activity activity, Handler handler, Callback callback) {
- Validate.notNull(activity);
- Validate.notNull(handler);
- Validate.notNull(callback);
-
- this.activity = activity;
- this.handler = handler;
- this.callback = callback;
- }
-
- public void onResume() {
- if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed");
- activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED));
- connectToDownloadService();
- }
-
- public void onPause() {
- if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver paused");
- try {
- activity.unregisterReceiver(contentChangedReceiver);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- }
- try {
- activity.unbindService(mConnection);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- }
- stopRefresher();
- }
-
- private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- // reconnect to DownloadService if connection has been closed
- if (downloadService == null) {
- connectToDownloadService();
- }
- callback.onContentChanged();
- startRefresher();
- }
- };
-
- public interface Callback {
- void onContentChanged();
-
- void onDownloadDataAvailable(List<Downloader> downloaderList);
- }
-
- private void connectToDownloadService() {
- activity.bindService(new Intent(activity, DownloadService.class), mConnection, 0);
- }
-
- private ServiceConnection mConnection = new ServiceConnection() {
- public void onServiceDisconnected(ComponentName className) {
- downloadService = null;
- mIsBound.set(false);
- stopRefresher();
- Log.i(TAG, "Closed connection with DownloadService.");
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- downloadService = ((DownloadService.LocalBinder) service)
- .getService();
- mIsBound.set(true);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Connection to service established");
- List<Downloader> downloaderList = downloadService.getDownloads();
- if (downloaderList != null && !downloaderList.isEmpty()) {
- callback.onDownloadDataAvailable(downloaderList);
- startRefresher();
- }
- }
- };
-
- private void stopRefresher() {
- if (refresherThread != null) {
- refresherThread.interrupt();
- }
- }
-
- private void startRefresher() {
- if (refresherThread == null || refresherThread.isInterrupted()) {
- refresherThread = new Thread(new RefresherThread());
- refresherThread.start();
- }
- }
-
- private class RefresherThread implements Runnable {
-
- public void run() {
- refresherThreadRunning.set(true);
- while (!Thread.interrupted()) {
- try {
- Thread.sleep(WAITING_INTERVAL_MS);
- } catch (InterruptedException e) {
- Log.d(TAG, "Refresher thread was interrupted");
- }
- if (mIsBound.get()) {
- postUpdate();
- }
- }
- refresherThreadRunning.set(false);
- }
-
- private void postUpdate() {
- handler.post(new Runnable() {
- @Override
- public void run() {
- callback.onContentChanged();
- if (downloadService != null) {
- List<Downloader> downloaderList = downloadService.getDownloads();
- if (downloaderList == null || downloaderList.isEmpty()) {
- Thread.currentThread().interrupt();
- }
- }
- }
- });
- }
- }
-
- public void setActivity(Activity activity) {
- Validate.notNull(activity);
- this.activity = activity;
- }
-
-}
-
diff --git a/src/de/danoeh/antennapod/asynctask/FeedRemover.java b/src/de/danoeh/antennapod/asynctask/FeedRemover.java
deleted file mode 100644
index 0549a4255..000000000
--- a/src/de/danoeh/antennapod/asynctask/FeedRemover.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package de.danoeh.antennapod.asynctask;
-
-import android.annotation.SuppressLint;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
-import android.os.AsyncTask;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.storage.DBWriter;
-
-import java.util.concurrent.ExecutionException;
-
-/** Removes a feed in the background. */
-public class FeedRemover extends AsyncTask<Void, Void, Void> {
- Context context;
- ProgressDialog dialog;
- Feed feed;
-
- public FeedRemover(Context context, Feed feed) {
- super();
- this.context = context;
- this.feed = feed;
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- try {
- DBWriter.deleteFeed(context, feed.getId()).get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- return null;
- }
-
- @Override
- protected void onCancelled() {
- dialog.dismiss();
- }
-
- @Override
- protected void onPostExecute(Void result) {
- dialog.dismiss();
- }
-
- @Override
- protected void onPreExecute() {
- dialog = new ProgressDialog(context);
- dialog.setMessage(context.getString(R.string.feed_remover_msg));
- dialog.setOnCancelListener(new OnCancelListener() {
-
- @Override
- public void onCancel(DialogInterface dialog) {
- cancel(true);
-
- }
-
- });
- dialog.show();
- }
-
- @SuppressLint("NewApi")
- public void executeAsync() {
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- execute();
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java b/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
deleted file mode 100644
index 9210ac1d1..000000000
--- a/src/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
+++ /dev/null
@@ -1,238 +0,0 @@
-package de.danoeh.antennapod.asynctask;
-
-import android.annotation.TargetApi;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-import android.widget.Toast;
-
-import org.apache.commons.lang3.Validate;
-import org.shredzone.flattr4j.exception.FlattrException;
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.FlattrAuthActivity;
-import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.NetworkUtils;
-import de.danoeh.antennapod.util.flattr.FlattrThing;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
-
-/**
- * Performs a click action in a background thread.
- * <p/>
- * When started, the flattr click worker will try to flattr every item that is in the flattr queue. If no network
- * connection is available it will shut down immediately. The FlattrClickWorker can also be given one additional
- * FlattrThing which will be flattrd immediately.
- * <p/>
- * The FlattrClickWorker will display a toast notification for every item that has been flattrd. If the FlattrClickWorker failed
- * to flattr something, a notification will be displayed.
- */
-public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorker.ExitCode> {
- protected static final String TAG = "FlattrClickWorker";
-
- private static final int NOTIFICATION_ID = 4;
-
- private final Context context;
-
- public static enum ExitCode {EXIT_NORMAL, NO_TOKEN, NO_NETWORK, NO_THINGS}
-
- private volatile int countFailed = 0;
- private volatile int countSuccess = 0;
-
- private volatile FlattrThing extraFlattrThing;
-
- /**
- * Only relevant if just one thing is flattrd
- */
- private volatile FlattrException exception;
-
- /**
- * Creates a new FlattrClickWorker which will only flattr all things in the queue.
- * <p/>
- * The FlattrClickWorker has to be started by calling executeAsync().
- *
- * @param context A context for accessing the database and posting notifications. Must not be null.
- */
- public FlattrClickWorker(Context context) {
- Validate.notNull(context);
- this.context = context.getApplicationContext();
- }
-
- /**
- * Creates a new FlattrClickWorker which will flattr all things in the queue and one additional
- * FlattrThing.
- * <p/>
- * The FlattrClickWorker has to be started by calling executeAsync().
- *
- * @param context A context for accessing the database and posting notifications. Must not be null.
- * @param extraFlattrThing The additional thing to flattr
- */
- public FlattrClickWorker(Context context, FlattrThing extraFlattrThing) {
- this(context);
- this.extraFlattrThing = extraFlattrThing;
- }
-
-
- @Override
- protected ExitCode doInBackground(Void... params) {
-
- if (!FlattrUtils.hasToken()) {
- return ExitCode.NO_TOKEN;
- }
-
- if (!NetworkUtils.networkAvailable(context)) {
- return ExitCode.NO_NETWORK;
- }
-
- final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue(context);
- if (extraFlattrThing != null) {
- flattrQueue.add(extraFlattrThing);
- } else if (flattrQueue.size() == 1) {
- // if only one item is flattrd, the report can specifically mentioned that this item has failed
- extraFlattrThing = flattrQueue.get(0);
- }
-
- if (flattrQueue.isEmpty()) {
- return ExitCode.NO_THINGS;
- }
-
- List<Future> dbFutures = new LinkedList<Future>();
- for (FlattrThing thing : flattrQueue) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Processing " + thing.getTitle());
-
- try {
- thing.getFlattrStatus().setUnflattred(); // pop from queue to prevent unflattrable things from getting stuck in flattr queue infinitely
- FlattrUtils.clickUrl(context, thing.getPaymentLink());
- thing.getFlattrStatus().setFlattred();
- publishProgress(R.string.flattr_click_success);
- countSuccess++;
-
- } catch (FlattrException e) {
- e.printStackTrace();
- countFailed++;
- if (countFailed == 1) {
- exception = e;
- }
- }
-
- Future<?> f = DBWriter.setFlattredStatus(context, thing, false);
- if (f != null) {
- dbFutures.add(f);
- }
- }
-
- for (Future f : dbFutures) {
- try {
- f.get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
-
- return ExitCode.EXIT_NORMAL;
- }
-
- @Override
- protected void onPostExecute(ExitCode exitCode) {
- super.onPostExecute(exitCode);
- switch (exitCode) {
- case EXIT_NORMAL:
- if (countFailed > 0) {
- postFlattrFailedNotification();
- }
- break;
- case NO_NETWORK:
- postToastNotification(R.string.flattr_click_enqueued);
- break;
- case NO_TOKEN:
- postNoTokenNotification();
- break;
- case NO_THINGS: // nothing to notify here
- break;
- }
- }
-
- @Override
- protected void onProgressUpdate(Integer... values) {
- super.onProgressUpdate(values);
- postToastNotification(values[0]);
- }
-
- private void postToastNotification(int msg) {
- Toast.makeText(context, context.getString(msg), Toast.LENGTH_LONG).show();
- }
-
- private void postNoTokenNotification() {
- PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, FlattrAuthActivity.class), 0);
-
- Notification notification = new NotificationCompat.Builder(context)
- .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg)))
- .setContentIntent(contentIntent)
- .setContentTitle(context.getString(R.string.no_flattr_token_title))
- .setTicker(context.getString(R.string.no_flattr_token_title))
- .setSmallIcon(R.drawable.stat_notify_sync_error)
- .setOngoing(false)
- .setAutoCancel(true)
- .build();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification);
- }
-
- private void postFlattrFailedNotification() {
- if (countFailed == 0) {
- return;
- }
-
- PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0);
- String title;
- String subtext;
-
- if (countFailed == 1) {
- title = context.getString(R.string.flattrd_failed_label);
- String exceptionMsg = (exception.getMessage() != null) ? exception.getMessage() : "";
- subtext = context.getString(R.string.flattr_click_failure, extraFlattrThing.getTitle())
- + "\n" + exceptionMsg;
- } else {
- title = context.getString(R.string.flattrd_label);
- subtext = context.getString(R.string.flattr_click_success_count, countSuccess) + "\n"
- + context.getString(R.string.flattr_click_failure_count, countFailed);
- }
-
- Notification notification = new NotificationCompat.Builder(context)
- .setStyle(new NotificationCompat.BigTextStyle().bigText(subtext))
- .setContentIntent(contentIntent)
- .setContentTitle(title)
- .setTicker(title)
- .setSmallIcon(R.drawable.stat_notify_sync_error)
- .setOngoing(false)
- .setAutoCancel(true)
- .build();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification);
- }
-
-
- /**
- * Starts the FlattrClickWorker as an AsyncTask.
- */
- @TargetApi(11)
- public void executeAsync() {
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- execute();
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java b/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
deleted file mode 100644
index 04d349671..000000000
--- a/src/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package de.danoeh.antennapod.asynctask;
-
-import android.content.Context;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
-import org.shredzone.flattr4j.exception.FlattrException;
-import org.shredzone.flattr4j.model.Flattr;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-
-/**
- * Fetch list of flattred things and flattr status in database in a background thread.
- */
-
-public class FlattrStatusFetcher extends Thread {
- protected static final String TAG = "FlattrStatusFetcher";
- protected Context context;
-
- public FlattrStatusFetcher(Context context) {
- super();
- this.context = context;
- }
-
- @Override
- public void run() {
- if (BuildConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status");
-
- Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
-
- try {
- List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings();
- DBWriter.setFlattredStatus(context, flattredThings).get();
- } catch (FlattrException e) {
- e.printStackTrace();
- Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage());
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status");
- }
-}
diff --git a/src/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java b/src/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java
deleted file mode 100644
index 0dcf832f7..000000000
--- a/src/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package de.danoeh.antennapod.asynctask;
-
-
-import android.annotation.SuppressLint;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.FlattrAuthActivity;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
-import org.shredzone.flattr4j.exception.FlattrException;
-import org.shredzone.flattr4j.oauth.AccessToken;
-import org.shredzone.flattr4j.oauth.AndroidAuthenticator;
-
-/** Fetches the access token in the background in order to avoid networkOnMainThread exception. */
-
-public class FlattrTokenFetcher extends AsyncTask<Void, Void, AccessToken> {
- private static final String TAG = "FlattrTokenFetcher";
- Context context;
- AndroidAuthenticator auth;
- AccessToken token;
- Uri uri;
- ProgressDialog dialog;
- FlattrException exception;
-
- public FlattrTokenFetcher(Context context, AndroidAuthenticator auth, Uri uri) {
- super();
- this.context = context;
- this.auth = auth;
- this.uri = uri;
- }
-
- @Override
- protected void onPostExecute(AccessToken result) {
- if (result != null) {
- FlattrUtils.storeToken(result);
- }
- dialog.dismiss();
- if (exception == null) {
- FlattrAuthActivity instance = FlattrAuthActivity.getInstance();
- if (instance != null) {
- instance.handleAuthenticationSuccess();
- } else {
- Log.e(TAG, "FlattrAuthActivity instance was null");
- }
- } else {
- FlattrUtils.showErrorDialog(context, exception.getMessage());
- }
- }
-
-
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- dialog = new ProgressDialog(context);
- dialog.setMessage(context.getString(R.string.processing_label));
- dialog.setIndeterminate(true);
- dialog.setCancelable(false);
- dialog.show();
- }
-
-
-
- @Override
- protected AccessToken doInBackground(Void... params) {
- try {
- token = auth.fetchAccessToken(uri);
- } catch (FlattrException e) {
- e.printStackTrace();
- exception = e;
- return null;
- }
- if (token != null) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Successfully got token");
- return token;
- } else {
- Log.w(TAG, "Flattr token was null");
- return null;
- }
- }
-
- @SuppressLint("NewApi")
- public void executeAsync() {
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- execute();
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java b/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
deleted file mode 100644
index 4abb1a67d..000000000
--- a/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
+++ /dev/null
@@ -1,114 +0,0 @@
-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 de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.opml.OpmlWriter;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.util.LangUtils;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-
-/** 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";
- 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, PodcastApp.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/src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
deleted file mode 100644
index 038b8dcc5..000000000
--- a/src/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
+++ /dev/null
@@ -1,69 +0,0 @@
-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.R;
-import de.danoeh.antennapod.activity.OpmlImportHolder;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.opml.OpmlElement;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java b/src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
deleted file mode 100644
index 13534fa64..000000000
--- a/src/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
+++ /dev/null
@@ -1,116 +0,0 @@
-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.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.opml.OpmlElement;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/asynctask/PicassoImageResource.java b/src/de/danoeh/antennapod/asynctask/PicassoImageResource.java
deleted file mode 100644
index 26f9d9278..000000000
--- a/src/de/danoeh/antennapod/asynctask/PicassoImageResource.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package de.danoeh.antennapod.asynctask;
-
-import android.net.Uri;
-
-/**
- * Classes that implement this interface provide access to an image resource that can
- * be loaded by the Picasso library.
- */
-public interface PicassoImageResource {
-
- /**
- * This scheme should be used by PicassoImageResources to
- * indicate that the image Uri points to a file that is not an image
- * (e.g. a media file). This workaround is needed so that the Picasso library
- * loads these Uri with a Downloader instead of trying to load it directly.
- * <p/>
- * For example implementations, see FeedMedia or ExternalMedia.
- */
- public static final String SCHEME_MEDIA = "media";
-
-
- /**
- * Parameter key for an encoded fallback Uri. This Uri MUST point to a local image file
- */
- public static final String PARAM_FALLBACK = "fallback";
-
- /**
- * Returns a Uri to the image or null if no image is available.
- * <p/>
- * The Uri can either be an HTTP-URL, a URL pointing to a local image file or
- * a non-image file (see SCHEME_MEDIA for more details).
- * <p/>
- * The Uri can also have an optional fallback-URL if loading the default URL
- * failed (see PARAM_FALLBACK).
- */
- public Uri getImageUri();
-}
diff --git a/src/de/danoeh/antennapod/asynctask/PicassoProvider.java b/src/de/danoeh/antennapod/asynctask/PicassoProvider.java
deleted file mode 100644
index 849725630..000000000
--- a/src/de/danoeh/antennapod/asynctask/PicassoProvider.java
+++ /dev/null
@@ -1,152 +0,0 @@
-package de.danoeh.antennapod.asynctask;
-
-import android.content.Context;
-import android.media.MediaMetadataRetriever;
-import android.net.Uri;
-import android.util.Log;
-import android.webkit.MimeTypeMap;
-
-import com.squareup.picasso.Cache;
-import com.squareup.picasso.Downloader;
-import com.squareup.picasso.LruCache;
-import com.squareup.picasso.OkHttpDownloader;
-import com.squareup.picasso.Picasso;
-
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/**
- * Provides access to Picasso instances.
- */
-public class PicassoProvider {
- private static final String TAG = "PicassoProvider";
-
- private static final boolean DEBUG = false;
-
- private static ExecutorService executorService;
- private static Cache memoryCache;
-
- private static Picasso defaultPicassoInstance;
- private static Picasso mediaMetadataPicassoInstance;
-
- private static synchronized ExecutorService getExecutorService() {
- if (executorService == null) {
- executorService = Executors.newFixedThreadPool(3);
- }
- return executorService;
- }
-
- private static synchronized Cache getMemoryCache(Context context) {
- if (memoryCache == null) {
- memoryCache = new LruCache(context);
- }
- return memoryCache;
- }
-
- /**
- * Returns a Picasso instance that uses an OkHttpDownloader. This instance can only load images
- * from image files.
- * <p/>
- * This instance should be used as long as no images from media files are loaded.
- */
- public static synchronized Picasso getDefaultPicassoInstance(Context context) {
- Validate.notNull(context);
- if (defaultPicassoInstance == null) {
- defaultPicassoInstance = new Picasso.Builder(context)
- .indicatorsEnabled(DEBUG)
- .loggingEnabled(DEBUG)
- .downloader(new OkHttpDownloader(context))
- .executor(getExecutorService())
- .memoryCache(getMemoryCache(context))
- .listener(new Picasso.Listener() {
- @Override
- public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) {
- Log.e(TAG, "Failed to load Uri:" + uri.toString());
- e.printStackTrace();
- }
- })
- .build();
- }
- return defaultPicassoInstance;
- }
-
- /**
- * Returns a Picasso instance that uses a MediaMetadataRetriever if the given Uri is a media file
- * and a default OkHttpDownloader otherwise.
- */
- public static synchronized Picasso getMediaMetadataPicassoInstance(Context context) {
- Validate.notNull(context);
- if (mediaMetadataPicassoInstance == null) {
- mediaMetadataPicassoInstance = new Picasso.Builder(context)
- .indicatorsEnabled(DEBUG)
- .loggingEnabled(DEBUG)
- .downloader(new MediaMetadataDownloader(context))
- .executor(getExecutorService())
- .memoryCache(getMemoryCache(context))
- .listener(new Picasso.Listener() {
- @Override
- public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) {
- Log.e(TAG, "Failed to load Uri:" + uri.toString());
- e.printStackTrace();
- }
- })
- .build();
- }
- return mediaMetadataPicassoInstance;
- }
-
- private static class MediaMetadataDownloader implements Downloader {
-
- private static final String TAG = "MediaMetadataDownloader";
-
- private final OkHttpDownloader okHttpDownloader;
-
- public MediaMetadataDownloader(Context context) {
- Validate.notNull(context);
- okHttpDownloader = new OkHttpDownloader(context);
- }
-
- @Override
- public Response load(Uri uri, boolean b) throws IOException {
- if (StringUtils.equals(uri.getScheme(), PicassoImageResource.SCHEME_MEDIA)) {
- String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(uri.getLastPathSegment()));
- if (StringUtils.startsWith(type, "image")) {
- File imageFile = new File(uri.toString());
- return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length());
- } else {
- MediaMetadataRetriever mmr = new MediaMetadataRetriever();
- mmr.setDataSource(uri.getPath());
- byte[] data = mmr.getEmbeddedPicture();
- mmr.release();
-
- if (data != null) {
- return new Response(new ByteArrayInputStream(data), true, data.length);
- } else {
-
- // check for fallback Uri
- String fallbackParam = uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK);
-
- if (fallbackParam != null) {
- String fallback = Uri.decode(Uri.parse(fallbackParam).getPath());
- if (fallback != null) {
- File imageFile = new File(fallback);
- return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length());
- }
- }
- return null;
- }
- }
- }
- return okHttpDownloader.load(uri, b);
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/backup/OpmlBackupAgent.java b/src/de/danoeh/antennapod/backup/OpmlBackupAgent.java
deleted file mode 100644
index 56d1ca092..000000000
--- a/src/de/danoeh/antennapod/backup/OpmlBackupAgent.java
+++ /dev/null
@@ -1,212 +0,0 @@
-package de.danoeh.antennapod.backup;
-
-import android.app.backup.BackupAgentHelper;
-import android.app.backup.BackupDataInputStream;
-import android.app.backup.BackupDataOutput;
-import android.app.backup.BackupHelper;
-import android.content.Context;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import de.danoeh.antennapod.BuildConfig;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
-import java.math.BigInteger;
-import java.security.DigestInputStream;
-import java.security.DigestOutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-
-import de.danoeh.antennapod.AppConfig;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.opml.OpmlElement;
-import de.danoeh.antennapod.opml.OpmlReader;
-import de.danoeh.antennapod.opml.OpmlWriter;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.util.LangUtils;
-
-public class OpmlBackupAgent extends BackupAgentHelper {
- private static final String OPML_BACKUP_KEY = "opml";
-
- @Override
- public void onCreate() {
- addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this));
- }
-
- private static final void LOGD(String tag, String msg) {
- if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) {
- Log.d(tag, msg);
- }
- }
-
- private static final void LOGD(String tag, String msg, Throwable tr) {
- if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) {
- Log.d(tag, msg, tr);
- }
- }
-
- /** Class for backing up and restoring the OPML file. */
- private static class OpmlBackupHelper implements BackupHelper {
- private static final String TAG = "OpmlBackupHelper";
-
- private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml";
-
- private final Context mContext;
-
- /** Checksum of restored OPML file */
- private byte[] mChecksum;
-
- public OpmlBackupHelper(Context context) {
- mContext = context;
- }
-
- @Override
- public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
- Log.d(TAG, "Performing backup");
- ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
- MessageDigest digester = null;
- Writer writer;
-
- try {
- digester = MessageDigest.getInstance("MD5");
- writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester),
- LangUtils.UTF_8);
- } catch (NoSuchAlgorithmException e) {
- writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8);
- }
-
- try {
- // Write OPML
- new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer);
-
- // Compare checksum of new and old file to see if we need to perform a backup at all
- if (digester != null) {
- byte[] newChecksum = digester.digest();
- LOGD(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16));
-
- // Get the old checksum
- if (oldState != null) {
- FileInputStream inState = new FileInputStream(oldState.getFileDescriptor());
- int len = inState.read();
-
- if (len != -1) {
- byte[] oldChecksum = new byte[len];
- inState.read(oldChecksum);
- LOGD(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16));
-
- if (Arrays.equals(oldChecksum, newChecksum)) {
- LOGD(TAG, "Checksums are the same; won't backup");
- return;
- }
- }
- }
-
- writeNewStateDescription(newState, newChecksum);
- }
-
- LOGD(TAG, "Backing up OPML");
- byte[] bytes = byteStream.toByteArray();
- data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length);
- data.writeEntityData(bytes, bytes.length);
- } catch (IOException e) {
- Log.e(TAG, "Error during backup", e);
- } finally {
- if (writer != null) {
- try {
- writer.close();
- } catch (IOException e) {
- }
- }
- }
- }
-
- @Override
- public void restoreEntity(BackupDataInputStream data) {
- LOGD(TAG, "Backup restore");
-
- if (!OPML_ENTITY_KEY.equals(data.getKey())) {
- LOGD(TAG, "Unknown entity key: " + data.getKey());
- return;
- }
-
- MessageDigest digester = null;
- Reader reader;
-
- try {
- digester = MessageDigest.getInstance("MD5");
- reader = new InputStreamReader(new DigestInputStream(data, digester),
- LangUtils.UTF_8);
- } catch (NoSuchAlgorithmException e) {
- reader = new InputStreamReader(data, LangUtils.UTF_8);
- }
-
- try {
- ArrayList<OpmlElement> opmlElements = new OpmlReader().readDocument(reader);
- mChecksum = digester == null ? null : digester.digest();
- DownloadRequester downloader = DownloadRequester.getInstance();
- Date lastUpdated = new Date();
-
- for (OpmlElement opmlElem : opmlElements) {
- Feed feed = new Feed(opmlElem.getXmlUrl(), lastUpdated, opmlElem.getText());
-
- try {
- downloader.downloadFeed(mContext, feed);
- } catch (DownloadRequestException e) {
- LOGD(TAG, "Error while restoring/downloading feed", e);
- }
- }
- } catch (XmlPullParserException e) {
- Log.e(TAG, "Error while parsing the OPML file", e);
- } catch (IOException e) {
- Log.e(TAG, "Failed to restore OPML backup", e);
- } finally {
- if (reader != null) {
- try {
- reader.close();
- } catch (IOException e) {
- }
- }
- }
- }
-
- @Override
- public void writeNewStateDescription(ParcelFileDescriptor newState) {
- writeNewStateDescription(newState, mChecksum);
- }
-
- /**
- * Writes the new state description, which is the checksum of the OPML file.
- *
- * @param newState
- * @param checksum
- */
- private void writeNewStateDescription(ParcelFileDescriptor newState, byte[] checksum) {
- if (checksum == null) {
- return;
- }
-
- try {
- FileOutputStream outState = new FileOutputStream(newState.getFileDescriptor());
- outState.write(checksum.length);
- outState.write(checksum);
- outState.flush();
- outState.close();
- } catch (IOException e) {
- Log.e(TAG, "Failed to write new state description", e);
- }
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java b/src/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java
deleted file mode 100644
index d1ed795dc..000000000
--- a/src/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java
+++ /dev/null
@@ -1,107 +0,0 @@
-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.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/src/de/danoeh/antennapod/dialog/ConfirmationDialog.java b/src/de/danoeh/antennapod/dialog/ConfirmationDialog.java
deleted file mode 100644
index df71fff77..000000000
--- a/src/de/danoeh/antennapod/dialog/ConfirmationDialog.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package de.danoeh.antennapod.dialog;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-
-/**
- * Creates an AlertDialog which asks the user to confirm something. Other
- * classes can handle events like confirmation or cancellation.
- */
-public abstract class ConfirmationDialog {
- private static final String TAG = "ConfirmationDialog";
-
- Context context;
- int titleId;
- int messageId;
-
- public ConfirmationDialog(Context context, int titleId, int messageId) {
- this.context = context;
- this.titleId = titleId;
- this.messageId = messageId;
- }
-
- public void onCancelButtonPressed(DialogInterface dialog) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Dialog was cancelled");
- dialog.dismiss();
- }
-
- public abstract void onConfirmButtonPressed(DialogInterface dialog);
-
- public final AlertDialog createNewDialog() {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(titleId);
- builder.setMessage(messageId);
- builder.setPositiveButton(R.string.confirm_label,
- new DialogInterface.OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- onConfirmButtonPressed(dialog);
- }
- });
- builder.setNegativeButton(R.string.cancel_label,
- new DialogInterface.OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- onCancelButtonPressed(dialog);
- }
- });
- builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
-
- @Override
- public void onCancel(DialogInterface dialog) {
- onCancelButtonPressed(dialog);
- }
- });
- return builder.create();
- }
-}
diff --git a/src/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java b/src/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java
deleted file mode 100644
index e363a6911..000000000
--- a/src/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package de.danoeh.antennapod.dialog;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import de.danoeh.antennapod.R;
-
-/** Creates Alert Dialogs if a DownloadRequestException has happened. */
-public class DownloadRequestErrorDialogCreator {
- private DownloadRequestErrorDialogCreator() {
- }
-
- public static void newRequestErrorDialog(Context context,
- String errorMessage) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setNeutralButton(android.R.string.ok,
- new DialogInterface.OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- })
- .setTitle(R.string.download_error_request_error)
- .setMessage(
- context.getString(R.string.download_request_error_dialog_message_prefix)
- + errorMessage);
- builder.create().show();
- }
-}
diff --git a/src/de/danoeh/antennapod/dialog/FeedItemDialog.java b/src/de/danoeh/antennapod/dialog/FeedItemDialog.java
deleted file mode 100644
index 4cd6a379e..000000000
--- a/src/de/danoeh/antennapod/dialog/FeedItemDialog.java
+++ /dev/null
@@ -1,434 +0,0 @@
-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.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.ShownotesProvider;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/src/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java
deleted file mode 100644
index a9c596d2e..000000000
--- a/src/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java
+++ /dev/null
@@ -1,67 +0,0 @@
-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.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/dialog/TimeDialog.java b/src/de/danoeh/antennapod/dialog/TimeDialog.java
deleted file mode 100644
index bbd514640..000000000
--- a/src/de/danoeh/antennapod/dialog/TimeDialog.java
+++ /dev/null
@@ -1,138 +0,0 @@
-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.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/src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
deleted file mode 100644
index b009e76a7..000000000
--- a/src/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
+++ /dev/null
@@ -1,100 +0,0 @@
-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.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/src/de/danoeh/antennapod/feed/Chapter.java b/src/de/danoeh/antennapod/feed/Chapter.java
deleted file mode 100644
index d6151ee9f..000000000
--- a/src/de/danoeh/antennapod/feed/Chapter.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-public abstract class Chapter extends FeedComponent {
-
- /** Defines starting point in milliseconds. */
- protected long start;
- protected String title;
- protected String link;
-
- public Chapter() {
- }
-
- public Chapter(long start) {
- super();
- this.start = start;
- }
-
- public Chapter(long start, String title, FeedItem item, String link) {
- super();
- this.start = start;
- this.title = title;
- this.link = link;
- }
-
- public abstract int getChapterType();
-
- public long getStart() {
- return start;
- }
-
- public String getTitle() {
- return title;
- }
-
- public String getLink() {
- return link;
- }
-
- public void setStart(long start) {
- this.start = start;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public void setLink(String link) {
- this.link = link;
- }
-
- @Override
- public String getHumanReadableIdentifier() {
- return title;
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/EventDistributor.java b/src/de/danoeh/antennapod/feed/EventDistributor.java
deleted file mode 100644
index 5fb72048e..000000000
--- a/src/de/danoeh/antennapod/feed/EventDistributor.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import android.os.Handler;
-import android.util.Log;
-
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.BuildConfig;
-
-import java.util.AbstractQueue;
-import java.util.Observable;
-import java.util.Observer;
-import java.util.concurrent.ConcurrentLinkedQueue;
-
-/**
- * Notifies its observers about changes in the feed database. Observers can
- * register by retrieving an instance of this class and registering an
- * EventListener. When new events arrive, the EventDistributor will process the
- * event queue in a handler that runs on the main thread. The observers will only
- * be notified once if the event queue contains multiple elements.
- *
- * Events can be sent with the send* methods.
- */
-public class EventDistributor extends Observable {
- private static final String TAG = "EventDistributor";
-
- public static final int FEED_LIST_UPDATE = 1;
- public static final int UNREAD_ITEMS_UPDATE = 2;
- public static final int QUEUE_UPDATE = 4;
- public static final int DOWNLOADLOG_UPDATE = 8;
- public static final int PLAYBACK_HISTORY_UPDATE = 16;
- public static final int DOWNLOAD_QUEUED = 32;
- public static final int DOWNLOAD_HANDLED = 64;
-
- private Handler handler;
- private AbstractQueue<Integer> events;
-
- private static EventDistributor instance;
-
- private EventDistributor() {
- this.handler = new Handler();
- events = new ConcurrentLinkedQueue<Integer>();
- }
-
- public static synchronized EventDistributor getInstance() {
- if (instance == null) {
- instance = new EventDistributor();
- }
- return instance;
- }
-
- public void register(EventListener el) {
- addObserver(el);
- }
-
- public void unregister(EventListener el) {
- deleteObserver(el);
- }
-
- public void addEvent(Integer i) {
- events.offer(i);
- handler.post(new Runnable() {
-
- @Override
- public void run() {
- processEventQueue();
- }
- });
- }
-
- private void processEventQueue() {
- Integer result = 0;
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Processing event queue. Number of events: "
- + events.size());
- for (Integer current = events.poll(); current != null; current = events
- .poll()) {
- result |= current;
- }
- if (result != 0) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Notifying observers. Data: " + result);
- setChanged();
- notifyObservers(result);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Event queue didn't contain any new events. Observers will not be notified.");
- }
- }
-
- @Override
- public void addObserver(Observer observer) {
- super.addObserver(observer);
- Validate.isInstanceOf(EventListener.class, observer);
- }
-
- public void sendDownloadQueuedBroadcast() {
- addEvent(DOWNLOAD_QUEUED);
- }
-
- public void sendUnreadItemsUpdateBroadcast() {
- addEvent(UNREAD_ITEMS_UPDATE);
- }
-
- public void sendQueueUpdateBroadcast() {
- addEvent(QUEUE_UPDATE);
- }
-
- public void sendFeedUpdateBroadcast() {
- addEvent(FEED_LIST_UPDATE);
- }
-
- public void sendPlaybackHistoryUpdateBroadcast() {
- addEvent(PLAYBACK_HISTORY_UPDATE);
- }
-
- public void sendDownloadLogUpdateBroadcast() {
- addEvent(DOWNLOADLOG_UPDATE);
- }
-
- public void sendDownloadHandledBroadcast() {
- addEvent(DOWNLOAD_HANDLED);
- }
-
- public static abstract class EventListener implements Observer {
-
- @Override
- public void update(Observable observable, Object data) {
- if (observable instanceof EventDistributor
- && data instanceof Integer) {
- update((EventDistributor) observable, (Integer) data);
- }
- }
-
- public abstract void update(EventDistributor eventDistributor,
- Integer arg);
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java
deleted file mode 100644
index b5415c69c..000000000
--- a/src/de/danoeh/antennapod/feed/Feed.java
+++ /dev/null
@@ -1,445 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import android.content.Context;
-import android.net.Uri;
-
-import de.danoeh.antennapod.asynctask.PicassoImageResource;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.EpisodeFilter;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-import de.danoeh.antennapod.util.flattr.FlattrThing;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-
-/**
- * Data Object for a whole feed
- *
- * @author daniel
- */
-public class Feed extends FeedFile implements FlattrThing, PicassoImageResource {
- public static final int FEEDFILETYPE_FEED = 0;
- public static final String TYPE_RSS2 = "rss";
- public static final String TYPE_RSS091 = "rss";
- public static final String TYPE_ATOM1 = "atom";
-
- private String title;
- /**
- * Contains 'id'-element in Atom feed.
- */
- private String feedIdentifier;
- /**
- * Link to the website.
- */
- private String link;
- private String description;
- private String language;
- /**
- * Name of the author
- */
- private String author;
- private FeedImage image;
- private List<FeedItem> items;
- /**
- * Date of last refresh.
- */
- private Date lastUpdate;
- private FlattrStatus flattrStatus;
- private String paymentLink;
- /**
- * Feed type, for example RSS 2 or Atom
- */
- private String type;
-
- /**
- * Feed preferences
- */
- private FeedPreferences preferences;
-
- /**
- * This constructor is used for restoring a feed from the database.
- */
- public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink,
- String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
- String downloadUrl, boolean downloaded, FlattrStatus status) {
- super(fileUrl, downloadUrl, downloaded);
- this.id = id;
- this.title = title;
- if (lastUpdate != null) {
- this.lastUpdate = (Date) lastUpdate.clone();
- } else {
- this.lastUpdate = null;
- }
- this.link = link;
- this.description = description;
- this.paymentLink = paymentLink;
- this.author = author;
- this.language = language;
- this.type = type;
- this.feedIdentifier = feedIdentifier;
- this.image = image;
- this.flattrStatus = status;
-
- items = new ArrayList<FeedItem>();
- }
-
- /**
- * This constructor is used for test purposes and uses a default flattr status object.
- */
- public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink,
- String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
- String downloadUrl, boolean downloaded) {
- this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image,
- fileUrl, downloadUrl, downloaded, new FlattrStatus());
- }
-
- /**
- * This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized.
- */
- public Feed() {
- super();
- items = new ArrayList<FeedItem>();
- lastUpdate = new Date();
- this.flattrStatus = new FlattrStatus();
- }
-
- /**
- * This constructor is used for requesting a feed download (it must not be used for anything else!). It should NOT be
- * used if the title of the feed is already known.
- */
- public Feed(String url, Date lastUpdate) {
- super(null, url, false);
- this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
- this.flattrStatus = new FlattrStatus();
- }
-
- /**
- * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be
- * used if the title of the feed is already known.
- */
- public Feed(String url, Date lastUpdate, String title) {
- this(url, lastUpdate);
- this.title = title;
- this.flattrStatus = new FlattrStatus();
- }
-
- /**
- * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be
- * used if the title of the feed is already known.
- */
- public Feed(String url, Date lastUpdate, String title, String username, String password) {
- this(url, lastUpdate, title);
- preferences = new FeedPreferences(0, true, username, password);
- }
-
- /**
- * Returns the number of FeedItems where 'read' is false. If the 'display
- * only episodes' - preference is set to true, this method will only count
- * items with episodes.
- */
- public int getNumOfNewItems() {
- int count = 0;
- for (FeedItem item : items) {
- if (item.getState() == FeedItem.State.NEW) {
- if (!UserPreferences.isDisplayOnlyEpisodes()
- || item.getMedia() != null) {
- count++;
- }
- }
- }
- return count;
- }
-
- /**
- * Returns the number of FeedItems where the media started to play but
- * wasn't finished yet.
- */
- public int getNumOfStartedItems() {
- int count = 0;
-
- for (FeedItem item : items) {
- FeedItem.State state = item.getState();
- if (state == FeedItem.State.IN_PROGRESS
- || state == FeedItem.State.PLAYING) {
- count++;
- }
- }
- return count;
- }
-
- /**
- * Returns true if at least one item in the itemlist is unread.
- *
- * @param enableEpisodeFilter true if this method should only count items with episodes if
- * the 'display only episodes' - preference is set to true by the
- * user.
- */
- public boolean hasNewItems(boolean enableEpisodeFilter) {
- for (FeedItem item : items) {
- if (item.getState() == FeedItem.State.NEW) {
- if (!(enableEpisodeFilter && UserPreferences
- .isDisplayOnlyEpisodes()) || item.getMedia() != null) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * Returns the number of FeedItems.
- *
- * @param enableEpisodeFilter true if this method should only count items with episodes if
- * the 'display only episodes' - preference is set to true by the
- * user.
- */
- public int getNumOfItems(boolean enableEpisodeFilter) {
- if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) {
- return EpisodeFilter.countItemsWithEpisodes(items);
- } else {
- return items.size();
- }
- }
-
- /**
- * Returns the item at the specified index.
- *
- * @param enableEpisodeFilter true if this method should ignore items without episdodes if
- * the episodes filter has been enabled by the user.
- */
- public FeedItem getItemAtIndex(boolean enableEpisodeFilter, int position) {
- if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) {
- return EpisodeFilter.accessEpisodeByIndex(items, position);
- } else {
- return items.get(position);
- }
- }
-
- /**
- * Returns the value that uniquely identifies this Feed. If the
- * feedIdentifier attribute is not null, it will be returned. Else it will
- * try to return the title. If the title is not given, it will use the link
- * of the feed.
- */
- public String getIdentifyingValue() {
- if (feedIdentifier != null && !feedIdentifier.isEmpty()) {
- return feedIdentifier;
- } else if (download_url != null && !download_url.isEmpty()) {
- return download_url;
- } else if (title != null && !title.isEmpty()) {
- return title;
- } else {
- return link;
- }
- }
-
- @Override
- public String getHumanReadableIdentifier() {
- if (title != null) {
- return title;
- } else {
- return download_url;
- }
- }
-
- public void updateFromOther(Feed other) {
- super.updateFromOther(other);
- if (other.title != null) {
- title = other.title;
- }
- if (other.feedIdentifier != null) {
- feedIdentifier = other.feedIdentifier;
- }
- if (other.link != null) {
- link = other.link;
- }
- if (other.description != null) {
- description = other.description;
- }
- if (other.language != null) {
- language = other.language;
- }
- if (other.author != null) {
- author = other.author;
- }
- if (other.paymentLink != null) {
- paymentLink = other.paymentLink;
- }
- if (other.flattrStatus != null) {
- flattrStatus = other.flattrStatus;
- }
- }
-
- public boolean compareWithOther(Feed other) {
- if (super.compareWithOther(other)) {
- return true;
- }
- if (!title.equals(other.title)) {
- return true;
- }
- if (other.feedIdentifier != null) {
- if (feedIdentifier == null
- || !feedIdentifier.equals(other.feedIdentifier)) {
- return true;
- }
- }
- if (other.link != null) {
- if (link == null || !link.equals(other.link)) {
- return true;
- }
- }
- if (other.description != null) {
- if (description == null || !description.equals(other.description)) {
- return true;
- }
- }
- if (other.language != null) {
- if (language == null || !language.equals(other.language)) {
- return true;
- }
- }
- if (other.author != null) {
- if (author == null || !author.equals(other.author)) {
- return true;
- }
- }
- if (other.paymentLink != null) {
- if (paymentLink == null || !paymentLink.equals(other.paymentLink)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public int getTypeAsInt() {
- return FEEDFILETYPE_FEED;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public String getLink() {
- return link;
- }
-
- public void setLink(String link) {
- this.link = link;
- }
-
- public String getDescription() {
- return description;
- }
-
- public void setDescription(String description) {
- this.description = description;
- }
-
- public FeedImage getImage() {
- return image;
- }
-
- public void setImage(FeedImage image) {
- this.image = image;
- }
-
- public List<FeedItem> getItems() {
- return items;
- }
-
- public void setItems(List<FeedItem> list) {
- this.items = list;
- }
-
- public Date getLastUpdate() {
- return (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
- }
-
- public void setLastUpdate(Date lastUpdate) {
- this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
- }
-
- public String getFeedIdentifier() {
- return feedIdentifier;
- }
-
- public void setFeedIdentifier(String feedIdentifier) {
- this.feedIdentifier = feedIdentifier;
- }
-
- public void setFlattrStatus(FlattrStatus status) {
- this.flattrStatus = status;
- }
-
- public FlattrStatus getFlattrStatus() {
- return flattrStatus;
- }
-
- public String getPaymentLink() {
- return paymentLink;
- }
-
- public void setPaymentLink(String paymentLink) {
- this.paymentLink = paymentLink;
- }
-
- public String getLanguage() {
- return language;
- }
-
- public void setLanguage(String language) {
- this.language = language;
- }
-
- public String getAuthor() {
- return author;
- }
-
- public void setAuthor(String author) {
- this.author = author;
- }
-
- public String getType() {
- return type;
- }
-
- public void setType(String type) {
- this.type = type;
- }
-
- public void setPreferences(FeedPreferences preferences) {
- this.preferences = preferences;
- }
-
- public FeedPreferences getPreferences() {
- return preferences;
- }
-
- public void savePreferences(Context context) {
- DBWriter.setFeedPreferences(context, preferences);
- }
-
- @Override
- public void setId(long id) {
- super.setId(id);
- if (preferences != null) {
- preferences.setFeedID(id);
- }
- }
-
- @Override
- public Uri getImageUri() {
- if (image != null) {
- return image.getImageUri();
- } else {
- return null;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/FeedComponent.java b/src/de/danoeh/antennapod/feed/FeedComponent.java
deleted file mode 100644
index 48b243770..000000000
--- a/src/de/danoeh/antennapod/feed/FeedComponent.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-/**
- * Represents every possible component of a feed
- *
- * @author daniel
- */
-public abstract class FeedComponent {
-
- protected long id;
-
- public FeedComponent() {
- super();
- }
-
- public long getId() {
- return id;
- }
-
- public void setId(long id) {
- this.id = id;
- }
-
- /**
- * Update this FeedComponent's attributes with the attributes from another
- * FeedComponent. This method should only update attributes which where read from
- * the feed.
- */
- public void updateFromOther(FeedComponent other) {
- }
-
- /**
- * Compare's this FeedComponent's attribute values with another FeedComponent's
- * attribute values. This method will only compare attributes which were
- * read from the feed.
- *
- * @return true if attribute values are different, false otherwise
- */
- public boolean compareWithOther(FeedComponent other) {
- return false;
- }
-
-
- /**
- * Should return a non-null, human-readable String so that the item can be
- * identified by the user. Can be title, download-url, etc.
- */
- public abstract String getHumanReadableIdentifier();
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- FeedComponent that = (FeedComponent) o;
-
- if (id != that.id) return false;
-
- return true;
- }
-
- @Override
- public int hashCode() {
- return (int) (id ^ (id >>> 32));
- }
-} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/feed/FeedFile.java b/src/de/danoeh/antennapod/feed/FeedFile.java
deleted file mode 100644
index a05533ebc..000000000
--- a/src/de/danoeh/antennapod/feed/FeedFile.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import java.io.File;
-
-/**
- * Represents a component of a Feed that has to be downloaded
- */
-public abstract class FeedFile extends FeedComponent {
-
- protected String file_url;
- protected String download_url;
- protected boolean downloaded;
-
- /**
- * Creates a new FeedFile object.
- *
- * @param file_url The location of the FeedFile. If this is null, the downloaded-attribute
- * will automatically be set to false.
- * @param download_url The location where the FeedFile can be downloaded.
- * @param downloaded true if the FeedFile has been downloaded, false otherwise. This parameter
- * will automatically be interpreted as false if the file_url is null.
- */
- public FeedFile(String file_url, String download_url, boolean downloaded) {
- super();
- this.file_url = file_url;
- this.download_url = download_url;
- this.downloaded = (file_url != null) && downloaded;
- }
-
- public FeedFile() {
- this(null, null, false);
- }
-
- public abstract int getTypeAsInt();
-
- /**
- * Update this FeedFile's attributes with the attributes from another
- * FeedFile. This method should only update attributes which where read from
- * the feed.
- */
- public void updateFromOther(FeedFile other) {
- super.updateFromOther(other);
- this.download_url = other.download_url;
- }
-
- /**
- * Compare's this FeedFile's attribute values with another FeedFile's
- * attribute values. This method will only compare attributes which were
- * read from the feed.
- *
- * @return true if attribute values are different, false otherwise
- */
- public boolean compareWithOther(FeedFile other) {
- if (super.compareWithOther(other)) {
- return true;
- }
- if (!download_url.equals(other.download_url)) {
- return true;
- }
- return false;
- }
-
- /**
- * Returns true if the file exists at file_url.
- */
- public boolean fileExists() {
- if (file_url == null) {
- return false;
- } else {
- File f = new File(file_url);
- return f.exists();
- }
- }
-
- public String getFile_url() {
- return file_url;
- }
-
- /**
- * Changes the file_url of this FeedFile. Setting this value to
- * null will also set the downloaded-attribute to false.
- */
- public void setFile_url(String file_url) {
- this.file_url = file_url;
- if (file_url == null) {
- downloaded = false;
- }
- }
-
- public String getDownload_url() {
- return download_url;
- }
-
- public void setDownload_url(String download_url) {
- this.download_url = download_url;
- }
-
- public boolean isDownloaded() {
- return downloaded;
- }
-
- public void setDownloaded(boolean downloaded) {
- this.downloaded = downloaded;
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/FeedImage.java b/src/de/danoeh/antennapod/feed/FeedImage.java
deleted file mode 100644
index c588f5e71..000000000
--- a/src/de/danoeh/antennapod/feed/FeedImage.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import android.net.Uri;
-
-import de.danoeh.antennapod.asynctask.PicassoImageResource;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-
-
-
-public class FeedImage extends FeedFile implements PicassoImageResource {
- public static final int FEEDFILETYPE_FEEDIMAGE = 1;
-
- protected String title;
- protected FeedComponent owner;
-
- public FeedImage(String download_url, String title) {
- super(null, download_url, false);
- this.download_url = download_url;
- this.title = title;
- }
-
- public FeedImage(long id, String title, String file_url,
- String download_url, boolean downloaded) {
- super(file_url, download_url, downloaded);
- this.id = id;
- this.title = title;
- }
-
- @Override
- public String getHumanReadableIdentifier() {
- if (owner != null && owner.getHumanReadableIdentifier() != null) {
- return owner.getHumanReadableIdentifier();
- } else {
- return download_url;
- }
- }
-
- @Override
- public int getTypeAsInt() {
- return FEEDFILETYPE_FEEDIMAGE;
- }
-
- public FeedImage() {
- super();
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public FeedComponent getOwner() {
- return owner;
- }
-
- public void setOwner(FeedComponent owner) {
- this.owner = owner;
- }
-
- @Override
- public Uri getImageUri() {
- if (file_url != null && downloaded) {
- return Uri.fromFile(new File(file_url));
- } else {
- return null;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java
deleted file mode 100644
index 78091ea33..000000000
--- a/src/de/danoeh/antennapod/feed/FeedItem.java
+++ /dev/null
@@ -1,333 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import android.net.Uri;
-
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.asynctask.PicassoImageResource;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.util.ShownotesProvider;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-import de.danoeh.antennapod.util.flattr.FlattrThing;
-
-import java.io.InputStream;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.Callable;
-
-/**
- * Data Object for a XML message
- *
- * @author daniel
- */
-public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource {
-
- /**
- * The id/guid that can be found in the rss/atom feed. Might not be set.
- */
- private String itemIdentifier;
- private String title;
- /**
- * The description of a feeditem.
- */
- private String description;
- /**
- * The content of the content-encoded tag of a feeditem.
- */
- private String contentEncoded;
-
- private String link;
- private Date pubDate;
- private FeedMedia media;
-
- private Feed feed;
- private long feedId;
-
- private boolean read;
- private String paymentLink;
- private FlattrStatus flattrStatus;
- private List<Chapter> chapters;
- private FeedImage image;
-
- public FeedItem() {
- this.read = true;
- this.flattrStatus = new FlattrStatus();
- }
-
- /**
- * This constructor should be used for creating test objects.
- */
- public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed) {
- this.id = id;
- this.title = title;
- this.itemIdentifier = itemIdentifier;
- this.link = link;
- this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
- this.read = read;
- this.feed = feed;
- this.flattrStatus = new FlattrStatus();
- }
-
- public void updateFromOther(FeedItem other) {
- super.updateFromOther(other);
- if (other.title != null) {
- title = other.title;
- }
- if (other.getDescription() != null) {
- description = other.getDescription();
- }
- if (other.getContentEncoded() != null) {
- contentEncoded = other.contentEncoded;
- }
- if (other.link != null) {
- link = other.link;
- }
- if (other.pubDate != null && other.pubDate != pubDate) {
- pubDate = other.pubDate;
- }
- if (other.media != null) {
- if (media == null) {
- setMedia(other.media);
- } else if (media.compareWithOther(other)) {
- media.updateFromOther(other);
- }
- }
- if (other.paymentLink != null) {
- paymentLink = other.paymentLink;
- }
- if (other.chapters != null) {
- if (chapters == null) {
- chapters = other.chapters;
- }
- }
- if (image == null) {
- image = other.image;
- }
- }
-
- /**
- * Returns the value that uniquely identifies this FeedItem. If the
- * itemIdentifier attribute is not null, it will be returned. Else it will
- * try to return the title. If the title is not given, it will use the link
- * of the entry.
- */
- public String getIdentifyingValue() {
- if (itemIdentifier != null && !itemIdentifier.isEmpty()) {
- return itemIdentifier;
- } else if (title != null && !title.isEmpty()) {
- return title;
- } else {
- return link;
- }
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public String getDescription() {
- return description;
- }
-
- public void setDescription(String description) {
- this.description = description;
- }
-
- public String getLink() {
- return link;
- }
-
- public void setLink(String link) {
- this.link = link;
- }
-
- public Date getPubDate() {
- if (pubDate != null) {
- return (Date) pubDate.clone();
- } else {
- return null;
- }
- }
-
- public void setPubDate(Date pubDate) {
- if (pubDate != null) {
- this.pubDate = (Date) pubDate.clone();
- } else {
- this.pubDate = null;
- }
- }
-
- public FeedMedia getMedia() {
- return media;
- }
-
- /**
- * Sets the media object of this FeedItem. If the given
- * FeedMedia object is not null, it's 'item'-attribute value
- * will also be set to this item.
- */
- public void setMedia(FeedMedia media) {
- this.media = media;
- if (media != null && media.getItem() != this) {
- media.setItem(this);
- }
- }
-
- public Feed getFeed() {
- return feed;
- }
-
- public void setFeed(Feed feed) {
- this.feed = feed;
- }
-
- public boolean isRead() {
- return read || isInProgress();
- }
-
- public void setRead(boolean read) {
- this.read = read;
- }
-
- private boolean isInProgress() {
- return (media != null && media.isInProgress());
- }
-
- public String getContentEncoded() {
- return contentEncoded;
- }
-
- public void setContentEncoded(String contentEncoded) {
- this.contentEncoded = contentEncoded;
- }
-
- public void setFlattrStatus(FlattrStatus status) {
- this.flattrStatus = status;
- }
-
- public FlattrStatus getFlattrStatus() {
- return flattrStatus;
- }
-
- public String getPaymentLink() {
- return paymentLink;
- }
-
- public void setPaymentLink(String paymentLink) {
- this.paymentLink = paymentLink;
- }
-
- public List<Chapter> getChapters() {
- return chapters;
- }
-
- public void setChapters(List<Chapter> chapters) {
- this.chapters = chapters;
- }
-
- public String getItemIdentifier() {
- return itemIdentifier;
- }
-
- public void setItemIdentifier(String itemIdentifier) {
- this.itemIdentifier = itemIdentifier;
- }
-
- public boolean hasMedia() {
- return media != null;
- }
-
- private boolean isPlaying() {
- if (media != null) {
- return media.isPlaying();
- }
- return false;
- }
-
- @Override
- public Callable<String> loadShownotes() {
- return new Callable<String>() {
- @Override
- public String call() throws Exception {
-
- if (contentEncoded == null || description == null) {
- DBReader.loadExtraInformationOfFeedItem(PodcastApp.getInstance(), FeedItem.this);
-
- }
- return (contentEncoded != null) ? contentEncoded : description;
- }
- };
- }
-
- @Override
- public Uri getImageUri() {
- if (hasMedia()) {
- return media.getImageUri();
- } else if (feed != null) {
- return feed.getImageUri();
- } else {
- return null;
- }
- }
-
- public enum State {
- NEW, IN_PROGRESS, READ, PLAYING
- }
-
- public State getState() {
- if (hasMedia()) {
- if (isPlaying()) {
- return State.PLAYING;
- }
- if (isInProgress()) {
- return State.IN_PROGRESS;
- }
- }
- return (isRead() ? State.READ : State.NEW);
- }
-
- public long getFeedId() {
- return feedId;
- }
-
- public void setFeedId(long feedId) {
- this.feedId = feedId;
- }
-
- /**
- * Returns the image of this item or the image of the feed if this item does
- * not have its own image.
- */
- public FeedImage getImage() {
- return (hasItemImage()) ? image : feed.getImage();
- }
-
- public void setImage(FeedImage image) {
- this.image = image;
- if (image != null) {
- image.setOwner(this);
- }
- }
-
- /**
- * Returns true if this FeedItem has its own image, false otherwise.
- */
- public boolean hasItemImage() {
- return image != null;
- }
-
- /**
- * Returns true if this FeedItem has its own image and the image has been downloaded.
- */
- public boolean hasItemImageDownloaded() {
- return image != null && image.isDownloaded();
- }
-
- @Override
- public String getHumanReadableIdentifier() {
- return title;
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java
deleted file mode 100644
index 9298ebe8a..000000000
--- a/src/de/danoeh/antennapod/feed/FeedMedia.java
+++ /dev/null
@@ -1,411 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.Callable;
-
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.ChapterUtils;
-import de.danoeh.antennapod.util.playback.Playable;
-
-public class FeedMedia extends FeedFile implements Playable {
- private static final String TAG = "FeedMedia";
-
- public static final int FEEDFILETYPE_FEEDMEDIA = 2;
- public static final int PLAYABLE_TYPE_FEEDMEDIA = 1;
-
- public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId";
- public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId";
-
- private int duration;
- private int position; // Current position in file
- private int played_duration; // How many ms of this file have been played (for autoflattring)
- private long size; // File size in Byte
- private String mime_type;
- private volatile FeedItem item;
- private Date playbackCompletionDate;
-
- /* Used for loading item when restoring from parcel. */
- private long itemID;
-
- public FeedMedia(FeedItem i, String download_url, long size,
- String mime_type) {
- super(null, download_url, false);
- this.item = i;
- this.size = size;
- this.mime_type = mime_type;
- }
-
- public FeedMedia(long id, FeedItem item, int duration, int position,
- long size, String mime_type, String file_url, String download_url,
- boolean downloaded, Date playbackCompletionDate, int played_duration) {
- super(file_url, download_url, downloaded);
- this.id = id;
- this.item = item;
- this.duration = duration;
- this.position = position;
- this.played_duration = played_duration;
- this.size = size;
- this.mime_type = mime_type;
- this.playbackCompletionDate = playbackCompletionDate == null
- ? null : (Date) playbackCompletionDate.clone();
- }
-
- public FeedMedia(long id, FeedItem item) {
- super();
- this.id = id;
- this.item = item;
- }
-
- @Override
- public String getHumanReadableIdentifier() {
- if (item != null && item.getTitle() != null) {
- return item.getTitle();
- } else {
- return download_url;
- }
- }
-
- /**
- * Uses mimetype to determine the type of media.
- */
- public MediaType getMediaType() {
- if (mime_type == null || mime_type.isEmpty()) {
- return MediaType.UNKNOWN;
- } else {
- if (mime_type.startsWith("audio")) {
- return MediaType.AUDIO;
- } else if (mime_type.startsWith("video")) {
- return MediaType.VIDEO;
- } else if (mime_type.equals("application/ogg")) {
- return MediaType.AUDIO;
- }
- }
- return MediaType.UNKNOWN;
- }
-
- public void updateFromOther(FeedMedia other) {
- super.updateFromOther(other);
- if (other.size > 0) {
- size = other.size;
- }
- if (other.mime_type != null) {
- mime_type = other.mime_type;
- }
- }
-
- public boolean compareWithOther(FeedMedia other) {
- if (super.compareWithOther(other)) {
- return true;
- }
- if (other.mime_type != null) {
- if (mime_type == null || !mime_type.equals(other.mime_type)) {
- return true;
- }
- }
- if (other.size > 0 && other.size != size) {
- return true;
- }
- return false;
- }
-
- /**
- * Reads playback preferences to determine whether this FeedMedia object is
- * currently being played.
- */
- public boolean isPlaying() {
- return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA
- && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id;
- }
-
- @Override
- public int getTypeAsInt() {
- return FEEDFILETYPE_FEEDMEDIA;
- }
-
- public int getDuration() {
- return duration;
- }
-
- public void setDuration(int duration) {
- this.duration = duration;
- }
-
- public int getPlayedDuration() {
- return played_duration;
- }
-
- public void setPlayedDuration(int played_duration) {
- this.played_duration = played_duration;
- }
-
- public int getPosition() {
- return position;
- }
-
- public void setPosition(int position) {
- this.position = position;
- }
-
- public long getSize() {
- return size;
- }
-
- public void setSize(long size) {
- this.size = size;
- }
-
- public String getMime_type() {
- return mime_type;
- }
-
- public void setMime_type(String mime_type) {
- this.mime_type = mime_type;
- }
-
- public FeedItem getItem() {
- return item;
- }
-
- /**
- * Sets the item object of this FeedMedia. If the given
- * FeedItem object is not null, it's 'media'-attribute value
- * will also be set to this media object.
- */
- public void setItem(FeedItem item) {
- this.item = item;
- if (item != null && item.getMedia() != this) {
- item.setMedia(this);
- }
- }
-
- public Date getPlaybackCompletionDate() {
- return playbackCompletionDate == null
- ? null : (Date) playbackCompletionDate.clone();
- }
-
- public void setPlaybackCompletionDate(Date playbackCompletionDate) {
- this.playbackCompletionDate = playbackCompletionDate == null
- ? null : (Date) playbackCompletionDate.clone();
- }
-
- public boolean isInProgress() {
- return (this.position > 0);
- }
-
- public FeedImage getImage() {
- if (item != null) {
- return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage();
- }
- return null;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeLong(id);
- dest.writeLong(item.getId());
-
- dest.writeInt(duration);
- dest.writeInt(position);
- dest.writeLong(size);
- dest.writeString(mime_type);
- dest.writeString(file_url);
- dest.writeString(download_url);
- dest.writeByte((byte) ((downloaded) ? 1 : 0));
- dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0);
- dest.writeInt(played_duration);
- }
-
- @Override
- public void writeToPreferences(Editor prefEditor) {
- prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId());
- prefEditor.putLong(PREF_MEDIA_ID, id);
- }
-
- @Override
- public void loadMetadata() throws PlayableException {
- if (item == null && itemID != 0) {
- item = DBReader.getFeedItem(PodcastApp.getInstance(), itemID);
- }
- }
-
- @Override
- public void loadChapterMarks() {
- if (getChapters() == null && !localFileAvailable()) {
- ChapterUtils.loadChaptersFromStreamUrl(this);
- if (getChapters() != null && item != null) {
- DBWriter.setFeedItem(PodcastApp.getInstance(),
- item);
- }
- }
-
- }
-
- @Override
- public String getEpisodeTitle() {
- if (item == null) {
- return null;
- }
- if (getItem().getTitle() != null) {
- return getItem().getTitle();
- } else {
- return getItem().getIdentifyingValue();
- }
- }
-
- @Override
- public List<Chapter> getChapters() {
- if (item == null) {
- return null;
- }
- return getItem().getChapters();
- }
-
- @Override
- public String getWebsiteLink() {
- if (item == null) {
- return null;
- }
- return getItem().getLink();
- }
-
- @Override
- public String getFeedTitle() {
- if (item == null) {
- return null;
- }
- return getItem().getFeed().getTitle();
- }
-
- @Override
- public Object getIdentifier() {
- return id;
- }
-
- @Override
- public String getLocalMediaUrl() {
- return file_url;
- }
-
- @Override
- public String getStreamUrl() {
- return download_url;
- }
-
- @Override
- public String getPaymentLink() {
- if (item == null) {
- return null;
- }
- return getItem().getPaymentLink();
- }
-
- @Override
- public boolean localFileAvailable() {
- return isDownloaded() && file_url != null;
- }
-
- @Override
- public boolean streamAvailable() {
- return download_url != null;
- }
-
- @Override
- public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
- setPosition(newPosition);
- DBWriter.setFeedMediaPlaybackInformation(PodcastApp.getInstance(), this);
- }
-
- @Override
- public void onPlaybackStart() {
- }
-
- @Override
- public void onPlaybackCompleted() {
-
- }
-
- @Override
- public int getPlayableType() {
- return PLAYABLE_TYPE_FEEDMEDIA;
- }
-
- @Override
- public void setChapters(List<Chapter> chapters) {
- getItem().setChapters(chapters);
- }
-
- @Override
- public Callable<String> loadShownotes() {
- return new Callable<String>() {
- @Override
- public String call() throws Exception {
- if (item == null) {
- item = DBReader.getFeedItem(PodcastApp.getInstance(), itemID);
- }
- if (item.getContentEncoded() == null || item.getDescription() == null) {
- DBReader.loadExtraInformationOfFeedItem(PodcastApp.getInstance(), item);
-
- }
- return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription();
- }
- };
- }
-
- public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() {
- public FeedMedia createFromParcel(Parcel in) {
- final long id = in.readLong();
- final long itemID = in.readLong();
- FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(),
- in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt());
- result.itemID = itemID;
- return result;
- }
-
- public FeedMedia[] newArray(int size) {
- return new FeedMedia[size];
- }
- };
-
- @Override
- public Uri getImageUri() {
- final Uri feedImgUri = getFeedImageUri();
-
- if (localFileAvailable()) {
- Uri.Builder builder = new Uri.Builder();
- builder.scheme(SCHEME_MEDIA)
- .encodedPath(getLocalMediaUrl());
- if (feedImgUri != null) {
- builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString());
- }
- return builder.build();
- } else {
- return feedImgUri;
- }
- }
-
- private Uri getFeedImageUri() {
- if (item != null && item.getFeed() != null) {
- return item.getFeed().getImageUri();
- } else {
- return null;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/FeedPreferences.java b/src/de/danoeh/antennapod/feed/FeedPreferences.java
deleted file mode 100644
index 29bc5ef0c..000000000
--- a/src/de/danoeh/antennapod/feed/FeedPreferences.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import android.content.Context;
-import de.danoeh.antennapod.storage.DBWriter;
-import org.apache.commons.lang3.StringUtils;
-
-/**
- * Contains preferences for a single feed.
- */
-public class FeedPreferences {
-
- private long feedID;
- private boolean autoDownload;
- private String username;
- private String password;
-
- public FeedPreferences(long feedID, boolean autoDownload, String username, String password) {
- this.feedID = feedID;
- this.autoDownload = autoDownload;
- this.username = username;
- this.password = password;
- }
-
-
- /**
- * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the
- * comparison.
- *
- * @return True if the two objects are different.
- */
- public boolean compareWithOther(FeedPreferences other) {
- if (other == null)
- return true;
- if (!StringUtils.equals(username, other.username)) {
- return true;
- }
- if (!StringUtils.equals(password, other.password)) {
- return true;
- }
- return false;
- }
-
- /**
- * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded
- * from the update.
- */
- public void updateFromOther(FeedPreferences other) {
- if (other == null)
- return;
- this.username = other.username;
- this.password = other.password;
- }
-
- public long getFeedID() {
- return feedID;
- }
-
- public void setFeedID(long feedID) {
- this.feedID = feedID;
- }
-
- public boolean getAutoDownload() {
- return autoDownload;
- }
-
- public void setAutoDownload(boolean autoDownload) {
- this.autoDownload = autoDownload;
- }
-
- public void save(Context context) {
- DBWriter.setFeedPreferences(context, this);
- }
-
- public String getUsername() {
- return username;
- }
-
- public void setUsername(String username) {
- this.username = username;
- }
-
- public String getPassword() {
- return password;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/ID3Chapter.java b/src/de/danoeh/antennapod/feed/ID3Chapter.java
deleted file mode 100644
index 6dde7854e..000000000
--- a/src/de/danoeh/antennapod/feed/ID3Chapter.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-public class ID3Chapter extends Chapter {
- public static final int CHAPTERTYPE_ID3CHAPTER = 2;
-
- /**
- * Identifies the chapter in its ID3 tag. This attribute does not have to be
- * store in the DB and is only used for parsing.
- */
- private String id3ID;
-
- public ID3Chapter(String id3ID, long start) {
- super(start);
- this.id3ID = id3ID;
- }
-
- public ID3Chapter(long start, String title, FeedItem item, String link) {
- super(start, title, item, link);
- }
-
- @Override
- public String toString() {
- return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start="
- + start + ", url=" + link + "]";
- }
-
- @Override
- public int getChapterType() {
- return CHAPTERTYPE_ID3CHAPTER;
- }
-
- public String getId3ID() {
- return id3ID;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/feed/MediaType.java b/src/de/danoeh/antennapod/feed/MediaType.java
deleted file mode 100644
index 324d0a221..000000000
--- a/src/de/danoeh/antennapod/feed/MediaType.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-public enum MediaType {
- AUDIO, VIDEO, UNKNOWN
-}
diff --git a/src/de/danoeh/antennapod/feed/SearchResult.java b/src/de/danoeh/antennapod/feed/SearchResult.java
deleted file mode 100644
index 1cba389ec..000000000
--- a/src/de/danoeh/antennapod/feed/SearchResult.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-public class SearchResult {
- private FeedComponent component;
- /** Additional information (e.g. where it was found) */
- private String subtitle;
- /** Higher value means more importance */
- private int value;
-
- public SearchResult(FeedComponent component, int value, String subtitle) {
- super();
- this.component = component;
- this.value = value;
- this.subtitle = subtitle;
- }
-
- public FeedComponent getComponent() {
- return component;
- }
-
- public String getSubtitle() {
- return subtitle;
- }
-
- public void setSubtitle(String subtitle) {
- this.subtitle = subtitle;
- }
-
- public int getValue() {
- return value;
- }
-
-
-}
diff --git a/src/de/danoeh/antennapod/feed/SimpleChapter.java b/src/de/danoeh/antennapod/feed/SimpleChapter.java
deleted file mode 100644
index 3dab1b74d..000000000
--- a/src/de/danoeh/antennapod/feed/SimpleChapter.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-public class SimpleChapter extends Chapter {
- public static final int CHAPTERTYPE_SIMPLECHAPTER = 0;
-
- public SimpleChapter(long start, String title, FeedItem item, String link) {
- super(start, title, item, link);
- }
-
- @Override
- public int getChapterType() {
- return CHAPTERTYPE_SIMPLECHAPTER;
- }
-
- public void updateFromOther(SimpleChapter other) {
- super.updateFromOther(other);
- start = other.start;
- if (other.title != null) {
- title = other.title;
- }
- if (other.link != null) {
- link = other.link;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/feed/VorbisCommentChapter.java b/src/de/danoeh/antennapod/feed/VorbisCommentChapter.java
deleted file mode 100644
index 59844ae1f..000000000
--- a/src/de/danoeh/antennapod/feed/VorbisCommentChapter.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package de.danoeh.antennapod.feed;
-
-import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException;
-
-import java.util.concurrent.TimeUnit;
-
-public class VorbisCommentChapter extends Chapter {
- public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3;
-
- private static final int CHAPTERXXX_LENGTH = "chapterxxx".length();
-
- private int vorbisCommentId;
-
- public VorbisCommentChapter(int vorbisCommentId) {
- this.vorbisCommentId = vorbisCommentId;
- }
-
- public VorbisCommentChapter(long start, String title, FeedItem item,
- String link) {
- super(start, title, item, link);
- }
-
- @Override
- public String toString() {
- return "VorbisCommentChapter [id=" + id + ", title=" + title
- + ", link=" + link + ", start=" + start + "]";
- }
-
- public static long getStartTimeFromValue(String value)
- throws VorbisCommentReaderException {
- String[] parts = value.split(":");
- if (parts.length >= 3) {
- try {
- long hours = TimeUnit.MILLISECONDS.convert(
- Long.parseLong(parts[0]), TimeUnit.HOURS);
- long minutes = TimeUnit.MILLISECONDS.convert(
- Long.parseLong(parts[1]), TimeUnit.MINUTES);
- if (parts[2].contains("-->")) {
- parts[2] = parts[2].substring(0, parts[2].indexOf("-->"));
- }
- long seconds = TimeUnit.MILLISECONDS.convert(
- ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS);
- return hours + minutes + seconds;
- } catch (NumberFormatException e) {
- throw new VorbisCommentReaderException(e);
- }
- } else {
- throw new VorbisCommentReaderException("Invalid time string");
- }
- }
-
- /**
- * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx*
- *
- * @return the id of the chapter key or -1 if the id couldn't be read.
- * @throws VorbisCommentReaderException
- * */
- public static int getIDFromKey(String key)
- throws VorbisCommentReaderException {
- if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx
- try {
- String strId = key.substring(8, 10);
- return Integer.parseInt(strId);
- } catch (NumberFormatException e) {
- throw new VorbisCommentReaderException(e);
- }
- }
- throw new VorbisCommentReaderException("key is too short (" + key + ")");
- }
-
- /**
- * Get the string that comes after 'CHAPTERxxx', for example 'name' or
- * 'url'.
- */
- public static String getAttributeTypeFromKey(String key) {
- if (key.length() > CHAPTERXXX_LENGTH) {
- return key.substring(CHAPTERXXX_LENGTH, key.length());
- }
- return null;
- }
-
- @Override
- public int getChapterType() {
- return CHAPTERTYPE_VORBISCOMMENT_CHAPTER;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public void setLink(String link) {
- this.link = link;
- }
-
- public void setStart(long start) {
- this.start = start;
- }
-
- public int getVorbisCommentId() {
- return vorbisCommentId;
- }
-
- public void setVorbisCommentId(int vorbisCommentId) {
- this.vorbisCommentId = vorbisCommentId;
- }
-
-
-
-}
diff --git a/src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
deleted file mode 100644
index 082fe93fc..000000000
--- a/src/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
+++ /dev/null
@@ -1,196 +0,0 @@
-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.feed.EventDistributor;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/CoverFragment.java b/src/de/danoeh/antennapod/fragment/CoverFragment.java
deleted file mode 100644
index ffce518bf..000000000
--- a/src/de/danoeh/antennapod/fragment/CoverFragment.java
+++ /dev/null
@@ -1,105 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/src/de/danoeh/antennapod/fragment/DownloadLogFragment.java
deleted file mode 100644
index d81ba4b86..000000000
--- a/src/de/danoeh/antennapod/fragment/DownloadLogFragment.java
+++ /dev/null
@@ -1,121 +0,0 @@
-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.feed.EventDistributor;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
deleted file mode 100644
index 985673dd3..000000000
--- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
+++ /dev/null
@@ -1,238 +0,0 @@
-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.asynctask.PicassoProvider;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.util.playback.PlaybackController;
-
-/**
- * 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/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
deleted file mode 100644
index 04c7fbf8e..000000000
--- a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
+++ /dev/null
@@ -1,476 +0,0 @@
-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.feed.FeedItem;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.util.ShareUtils;
-import de.danoeh.antennapod.util.ShownotesProvider;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.util.playback.PlaybackController;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/ItemlistFragment.java b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java
deleted file mode 100644
index 909774467..000000000
--- a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java
+++ /dev/null
@@ -1,456 +0,0 @@
-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.asynctask.DownloadObserver;
-import de.danoeh.antennapod.asynctask.FeedRemover;
-import de.danoeh.antennapod.asynctask.PicassoProvider;
-import de.danoeh.antennapod.dialog.ConfirmationDialog;
-import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator;
-import de.danoeh.antennapod.dialog.FeedItemDialog;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.service.download.DownloadService;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java
deleted file mode 100644
index fe995256b..000000000
--- a/src/de/danoeh/antennapod/fragment/NewEpisodesFragment.java
+++ /dev/null
@@ -1,425 +0,0 @@
-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.asynctask.DownloadObserver;
-import de.danoeh.antennapod.dialog.FeedItemDialog;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.download.DownloadService;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
deleted file mode 100644
index 470186180..000000000
--- a/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
+++ /dev/null
@@ -1,288 +0,0 @@
-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.asynctask.DownloadObserver;
-import de.danoeh.antennapod.dialog.FeedItemDialog;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/fragment/QueueFragment.java b/src/de/danoeh/antennapod/fragment/QueueFragment.java
deleted file mode 100644
index 2f322f75b..000000000
--- a/src/de/danoeh/antennapod/fragment/QueueFragment.java
+++ /dev/null
@@ -1,383 +0,0 @@
-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.asynctask.DownloadObserver;
-import de.danoeh.antennapod.dialog.FeedItemDialog;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/src/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java
deleted file mode 100644
index 89c30e34b..000000000
--- a/src/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java
+++ /dev/null
@@ -1,69 +0,0 @@
-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.asynctask.DownloadObserver;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/SearchFragment.java b/src/de/danoeh/antennapod/fragment/SearchFragment.java
deleted file mode 100644
index b1411cf0a..000000000
--- a/src/de/danoeh/antennapod/fragment/SearchFragment.java
+++ /dev/null
@@ -1,258 +0,0 @@
-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.feed.*;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.FeedSearcher;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
deleted file mode 100644
index 1b4616207..000000000
--- a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
+++ /dev/null
@@ -1,169 +0,0 @@
-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.support.v7.widget.*;
-import android.util.Log;
-import android.view.*;
-import android.widget.*;
-import android.widget.SearchView;
-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.fragment.SearchFragment;
-import de.danoeh.antennapod.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java
deleted file mode 100644
index 5717a74e7..000000000
--- a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package de.danoeh.antennapod.fragment.gpodnet;
-
-import de.danoeh.antennapod.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java
deleted file mode 100644
index 801024787..000000000
--- a/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java
+++ /dev/null
@@ -1,80 +0,0 @@
-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.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java
deleted file mode 100644
index 45fe25580..000000000
--- a/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.danoeh.antennapod.fragment.gpodnet;
-
-import de.danoeh.antennapod.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
deleted file mode 100644
index 204dda992..000000000
--- a/src/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
+++ /dev/null
@@ -1,50 +0,0 @@
-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.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
deleted file mode 100644
index a7e1033df..000000000
--- a/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
+++ /dev/null
@@ -1,146 +0,0 @@
-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.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetTag;
-import de.danoeh.antennapod.util.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.util.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/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java
deleted file mode 100644
index 038b2a367..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java
+++ /dev/null
@@ -1,718 +0,0 @@
-package de.danoeh.antennapod.gpoddernet;
-
-import org.apache.commons.lang3.Validate;
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.auth.AuthenticationException;
-import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.ClientProtocolException;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.auth.BasicScheme;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.LinkedList;
-import java.util.List;
-
-import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetTag;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse;
-import de.danoeh.antennapod.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.service.download.AntennapodHttpClient;
-
-/**
- * Communicates with the gpodder.net service.
- */
-public class GpodnetService {
-
- private static final String BASE_SCHEME = "https";
-
- public static final String DEFAULT_BASE_HOST = "gpodder.net";
- private final String BASE_HOST;
-
- private final HttpClient httpClient;
-
- public GpodnetService() {
- httpClient = AntennapodHttpClient.getHttpClient();
- BASE_HOST = GpodnetPreferences.getHostname();
- }
-
- /**
- * Returns the [count] most used tags.
- */
- public List<GpodnetTag> getTopTags(int count)
- throws GpodnetServiceException {
- URI uri;
- try {
- uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/api/2/tags/%d.json", count), null);
- } catch (URISyntaxException e1) {
- e1.printStackTrace();
- throw new IllegalStateException(e1);
- }
-
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
- try {
- JSONArray jsonTagList = new JSONArray(response);
- List<GpodnetTag> tagList = new ArrayList<GpodnetTag>(
- jsonTagList.length());
- for (int i = 0; i < jsonTagList.length(); i++) {
- JSONObject jObj = jsonTagList.getJSONObject(i);
- String name = jObj.getString("tag");
- int usage = jObj.getInt("usage");
- tagList.add(new GpodnetTag(name, usage));
- }
- return tagList;
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
- /**
- * Returns the [count] most subscribed podcasts for the given tag.
- *
- * @throws IllegalArgumentException if tag is null
- */
- public List<GpodnetPodcast> getPodcastsForTag(GpodnetTag tag, int count)
- throws GpodnetServiceException {
- Validate.notNull(tag);
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/api/2/tag/%s/%d.json", tag.getName(), count), null);
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
-
- JSONArray jsonArray = new JSONArray(response);
- return readPodcastListFromJSONArray(jsonArray);
-
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
-
- }
- }
-
- /**
- * Returns the toplist of podcast.
- *
- * @param count of elements that should be returned. Must be in range 1..100.
- * @throws IllegalArgumentException if count is out of range.
- */
- public List<GpodnetPodcast> getPodcastToplist(int count)
- throws GpodnetServiceException {
- Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100");
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/toplist/%d.json", count), null);
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
-
- JSONArray jsonArray = new JSONArray(response);
- return readPodcastListFromJSONArray(jsonArray);
-
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
-
- }
- }
-
- /**
- * Returns a list of suggested podcasts for the user that is currently
- * logged in.
- * <p/>
- * This method requires authentication.
- *
- * @param count The
- * number of elements that should be returned. Must be in range
- * 1..100.
- * @throws IllegalArgumentException if count is out of range.
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public List<GpodnetPodcast> getSuggestions(int count) throws GpodnetServiceException {
- Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100");
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/suggestions/%d.json", count), null);
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
-
- JSONArray jsonArray = new JSONArray(response);
- return readPodcastListFromJSONArray(jsonArray);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
- /**
- * Searches the podcast directory for a given string.
- *
- * @param query The search query
- * @param scaledLogoSize The size of the logos that are returned by the search query.
- * Must be in range 1..256. If the value is out of range, the
- * default value defined by the gpodder.net API will be used.
- */
- public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize)
- throws GpodnetServiceException {
- String parameters = (scaledLogoSize > 0 && scaledLogoSize <= 256) ? String
- .format("q=%s&scale_logo=%d", query, scaledLogoSize) : String
- .format("q=%s", query);
- try {
- URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, "/search.json",
- parameters, null);
- System.out.println(uri.toASCIIString());
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
-
- JSONArray jsonArray = new JSONArray(response);
- return readPodcastListFromJSONArray(jsonArray);
-
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
-
- }
- }
-
- /**
- * Returns all devices of a given user.
- * <p/>
- * This method requires authentication.
- *
- * @param username The username. Must be the same user as the one which is
- * currently logged in.
- * @throws IllegalArgumentException If username is null.
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public List<GpodnetDevice> getDevices(String username)
- throws GpodnetServiceException {
- Validate.notNull(username);
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/api/2/devices/%s.json", username), null);
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
- JSONArray devicesArray = new JSONArray(response);
- List<GpodnetDevice> result = readDeviceListFromJSONArray(devicesArray);
-
- return result;
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
- /**
- * Configures the device of a given user.
- * <p/>
- * This method requires authentication.
- *
- * @param username The username. Must be the same user as the one which is
- * currently logged in.
- * @param deviceId The ID of the device that should be configured.
- * @throws IllegalArgumentException If username or deviceId is null.
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public void configureDevice(String username, String deviceId,
- String caption, GpodnetDevice.DeviceType type)
- throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/api/2/devices/%s/%s.json", username, deviceId), null);
- HttpPost request = new HttpPost(uri);
- if (caption != null || type != null) {
- JSONObject jsonContent = new JSONObject();
- if (caption != null) {
- jsonContent.put("caption", caption);
- }
- if (type != null) {
- jsonContent.put("type", type.toString());
- }
- StringEntity strEntity = new StringEntity(
- jsonContent.toString(), "UTF-8");
- strEntity.setContentType("application/json");
- request.setEntity(strEntity);
- }
- executeRequest(request);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalArgumentException(e);
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
- /**
- * Returns the subscriptions of a specific device.
- * <p/>
- * This method requires authentication.
- *
- * @param username The username. Must be the same user as the one which is
- * currently logged in.
- * @param deviceId The ID of the device whose subscriptions should be returned.
- * @return A list of subscriptions in OPML format.
- * @throws IllegalArgumentException If username or deviceId is null.
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public String getSubscriptionsOfDevice(String username, String deviceId)
- throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/subscriptions/%s/%s.opml", username, deviceId), null);
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
- return response;
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalArgumentException(e);
- }
- }
-
- /**
- * Returns all subscriptions of a specific user.
- * <p/>
- * This method requires authentication.
- *
- * @param username The username. Must be the same user as the one which is
- * currently logged in.
- * @return A list of subscriptions in OPML format.
- * @throws IllegalArgumentException If username is null.
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public String getSubscriptionsOfUser(String username)
- throws GpodnetServiceException {
- Validate.notNull(username);
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/subscriptions/%s.opml", username), null);
- HttpGet request = new HttpGet(uri);
- String response = executeRequest(request);
- return response;
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalArgumentException(e);
- }
- }
-
- /**
- * Uploads the subscriptions of a specific device.
- * <p/>
- * This method requires authentication.
- *
- * @param username The username. Must be the same user as the one which is
- * currently logged in.
- * @param deviceId The ID of the device whose subscriptions should be updated.
- * @param subscriptions A list of feed URLs containing all subscriptions of the
- * device.
- * @throws IllegalArgumentException If username, deviceId or subscriptions is null.
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public void uploadSubscriptions(String username, String deviceId,
- List<String> subscriptions) throws GpodnetServiceException {
- if (username == null || deviceId == null || subscriptions == null) {
- throw new IllegalArgumentException(
- "Username, device ID and subscriptions must not be null");
- }
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/subscriptions/%s/%s.txt", username, deviceId), null);
- HttpPut request = new HttpPut(uri);
- StringBuilder builder = new StringBuilder();
- for (String s : subscriptions) {
- builder.append(s);
- builder.append("\n");
- }
- StringEntity entity = new StringEntity(builder.toString(), "UTF-8");
- request.setEntity(entity);
-
- executeRequest(request);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- }
- }
-
- /**
- * Updates the subscription list of a specific device.
- * <p/>
- * This method requires authentication.
- *
- * @param username The username. Must be the same user as the one which is
- * currently logged in.
- * @param deviceId The ID of the device whose subscriptions should be updated.
- * @param added Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates
- * @param removed Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates
- * @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse}
- * for details.
- * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null.
- * @throws de.danoeh.antennapod.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there
- * is an authentication error.
- */
- public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection<String> added,
- Collection<String> removed) throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
- Validate.notNull(added);
- Validate.notNull(removed);
-
- try {
- URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/api/2/subscriptions/%s/%s.json", username, deviceId), null);
-
- final JSONObject requestObject = new JSONObject();
- requestObject.put("add", new JSONArray(added));
- requestObject.put("remove", new JSONArray(removed));
-
- HttpPost request = new HttpPost(uri);
- StringEntity entity = new StringEntity(requestObject.toString(), "UTF-8");
- request.setEntity(entity);
-
- final String response = executeRequest(request);
- return GpodnetUploadChangesResponse.fromJSONObject(response);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- }
-
- }
-
- /**
- * Returns all subscription changes of a specific device.
- * <p/>
- * This method requires authentication.
- *
- * @param username The username. Must be the same user as the one which is
- * currently logged in.
- * @param deviceId The ID of the device whose subscription changes should be
- * downloaded.
- * @param timestamp A timestamp that can be used to receive all changes since a
- * specific point in time.
- * @throws IllegalArgumentException If username or deviceId is null.
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public GpodnetSubscriptionChange getSubscriptionChanges(String username,
- String deviceId, long timestamp) throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(deviceId);
-
- String params = String.format("since=%d", timestamp);
- String path = String.format("/api/2/subscriptions/%s/%s.json",
- username, deviceId);
- try {
- URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params,
- null);
- HttpGet request = new HttpGet(uri);
-
- String response = executeRequest(request);
- JSONObject changes = new JSONObject(response);
- return readSubscriptionChangesFromJSONObject(changes);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new IllegalStateException(e);
- } catch (JSONException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
-
- }
-
- /**
- * Logs in a specific user. This method must be called if any of the methods
- * that require authentication is used.
- *
- * @throws IllegalArgumentException If username or password is null.
- */
- public void authenticate(String username, String password)
- throws GpodnetServiceException {
- Validate.notNull(username);
- Validate.notNull(password);
-
- URI uri;
- try {
- uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
- "/api/2/auth/%s/login.json", username), null);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- throw new GpodnetServiceException();
- }
- HttpPost request = new HttpPost(uri);
- executeRequestWithAuthentication(request, username, password);
- }
-
- /**
- * Shuts down the GpodnetService's HTTP client. The service will be shut down in a separate thread to avoid
- * NetworkOnMainThreadExceptions.
- */
- public void shutdown() {
- new Thread() {
- @Override
- public void run() {
- AntennapodHttpClient.cleanup();
- }
- }.start();
- }
-
- private String executeRequest(HttpRequestBase request)
- throws GpodnetServiceException {
- Validate.notNull(request);
-
- String responseString = null;
- HttpResponse response = null;
- try {
- response = httpClient.execute(request);
- checkStatusCode(response);
- responseString = getStringFromEntity(response.getEntity());
- } catch (ClientProtocolException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (IOException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } finally {
- if (response != null) {
- try {
- response.getEntity().consumeContent();
- } catch (IOException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
- }
- return responseString;
- }
-
- private String executeRequestWithAuthentication(HttpRequestBase request,
- String username, String password) throws GpodnetServiceException {
- if (request == null || username == null || password == null) {
- throw new IllegalArgumentException(
- "request and credentials must not be null");
- }
- String result = null;
- HttpResponse response = null;
- try {
- Header auth = new BasicScheme().authenticate(
- new UsernamePasswordCredentials(username, password),
- request);
- request.addHeader(auth);
- response = httpClient.execute(request);
- checkStatusCode(response);
- result = getStringFromEntity(response.getEntity());
- } catch (ClientProtocolException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (IOException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } catch (AuthenticationException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- } finally {
- if (response != null) {
- try {
- response.getEntity().consumeContent();
- } catch (IOException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
- }
- return result;
- }
-
- private String getStringFromEntity(HttpEntity entity)
- throws GpodnetServiceException {
- Validate.notNull(entity);
-
- ByteArrayOutputStream outputStream;
- int contentLength = (int) entity.getContentLength();
- if (contentLength > 0) {
- outputStream = new ByteArrayOutputStream(contentLength);
- } else {
- outputStream = new ByteArrayOutputStream();
- }
- try {
- byte[] buffer = new byte[8 * 1024];
- InputStream in = entity.getContent();
- int count;
- while ((count = in.read(buffer)) > 0) {
- outputStream.write(buffer, 0, count);
- }
- } catch (IOException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- // System.out.println(outputStream.toString());
- return outputStream.toString();
- }
-
- private void checkStatusCode(HttpResponse response)
- throws GpodnetServiceException {
- Validate.notNull(response);
- int responseCode = response.getStatusLine().getStatusCode();
- if (responseCode != HttpStatus.SC_OK) {
- if (responseCode == HttpStatus.SC_UNAUTHORIZED) {
- throw new GpodnetServiceAuthenticationException("Wrong username or password");
- } else {
- throw new GpodnetServiceBadStatusCodeException(
- "Bad response code: " + responseCode, responseCode);
- }
- }
- }
-
- private List<GpodnetPodcast> readPodcastListFromJSONArray(JSONArray array)
- throws JSONException {
- Validate.notNull(array);
-
- List<GpodnetPodcast> result = new ArrayList<GpodnetPodcast>(
- array.length());
- for (int i = 0; i < array.length(); i++) {
- result.add(readPodcastFromJSONObject(array.getJSONObject(i)));
- }
- return result;
-
- }
-
- private GpodnetPodcast readPodcastFromJSONObject(JSONObject object)
- throws JSONException {
- String url = object.getString("url");
-
- String title;
- Object titleObj = object.opt("title");
- if (titleObj != null && titleObj instanceof String) {
- title = (String) titleObj;
- } else {
- title = url;
- }
-
- String description;
- Object descriptionObj = object.opt("description");
- if (descriptionObj != null && descriptionObj instanceof String) {
- description = (String) descriptionObj;
- } else {
- description = "";
- }
-
- int subscribers = object.getInt("subscribers");
-
- Object logoUrlObj = object.opt("logo_url");
- String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj
- : null;
- if (logoUrl == null) {
- Object scaledLogoUrl = object.opt("scaled_logo_url");
- if (scaledLogoUrl != null && scaledLogoUrl instanceof String) {
- logoUrl = (String) scaledLogoUrl;
- }
- }
-
- String website = null;
- Object websiteObj = object.opt("website");
- if (websiteObj != null && websiteObj instanceof String) {
- website = (String) websiteObj;
- }
- String mygpoLink = object.getString("mygpo_link");
- return new GpodnetPodcast(url, title, description, subscribers,
- logoUrl, website, mygpoLink);
- }
-
- private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array)
- throws JSONException {
- Validate.notNull(array);
-
- List<GpodnetDevice> result = new ArrayList<GpodnetDevice>(
- array.length());
- for (int i = 0; i < array.length(); i++) {
- result.add(readDeviceFromJSONObject(array.getJSONObject(i)));
- }
- return result;
- }
-
- private GpodnetDevice readDeviceFromJSONObject(JSONObject object)
- throws JSONException {
- String id = object.getString("id");
- String caption = object.getString("caption");
- String type = object.getString("type");
- int subscriptions = object.getInt("subscriptions");
- return new GpodnetDevice(id, caption, type, subscriptions);
- }
-
- private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject(
- JSONObject object) throws JSONException {
- Validate.notNull(object);
-
- List<String> added = new LinkedList<String>();
- JSONArray jsonAdded = object.getJSONArray("add");
- for (int i = 0; i < jsonAdded.length(); i++) {
- added.add(jsonAdded.getString(i));
- }
-
- List<String> removed = new LinkedList<String>();
- JSONArray jsonRemoved = object.getJSONArray("remove");
- for (int i = 0; i < jsonRemoved.length(); i++) {
- removed.add(jsonRemoved.getString(i));
- }
-
- long timestamp = object.getLong("timestamp");
- return new GpodnetSubscriptionChange(added, removed, timestamp);
- }
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java
deleted file mode 100644
index 3b0140826..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.danoeh.antennapod.gpoddernet;
-
-public class GpodnetServiceAuthenticationException extends GpodnetServiceException {
-
- public GpodnetServiceAuthenticationException() {
- super();
- }
-
- public GpodnetServiceAuthenticationException(String message, Throwable cause) {
- super(message, cause);
- }
-
- public GpodnetServiceAuthenticationException(String message) {
- super(message);
- }
-
- public GpodnetServiceAuthenticationException(Throwable cause) {
- super(cause);
- }
-
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java
deleted file mode 100644
index a32e9357b..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package de.danoeh.antennapod.gpoddernet;
-
-public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException {
- int statusCode;
-
- public GpodnetServiceBadStatusCodeException(String message, int statusCode) {
- super(message);
- this.statusCode = statusCode;
- }
-
-
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java
deleted file mode 100644
index bdb394454..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.danoeh.antennapod.gpoddernet;
-
-public class GpodnetServiceException extends Exception {
-
- public GpodnetServiceException() {
- }
-
- public GpodnetServiceException(String message) {
- super(message);
- }
-
- public GpodnetServiceException(Throwable cause) {
- super(cause);
- }
-
- public GpodnetServiceException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java
deleted file mode 100644
index 86a2171fa..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package de.danoeh.antennapod.gpoddernet.model;
-
-import org.apache.commons.lang3.Validate;
-
-public class GpodnetDevice {
-
- private String id;
- private String caption;
- private DeviceType type;
- private int subscriptions;
-
- public GpodnetDevice(String id, String caption, String type,
- int subscriptions) {
- Validate.notNull(id);
-
- this.id = id;
- this.caption = caption;
- this.type = DeviceType.fromString(type);
- this.subscriptions = subscriptions;
- }
-
- @Override
- public String toString() {
- return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type="
- + type + ", subscriptions=" + subscriptions + "]";
- }
-
- public static enum DeviceType {
- DESKTOP, LAPTOP, MOBILE, SERVER, OTHER;
-
- static DeviceType fromString(String s) {
- if (s == null) {
- return OTHER;
- }
-
- if (s.equals("desktop")) {
- return DESKTOP;
- } else if (s.equals("laptop")) {
- return LAPTOP;
- } else if (s.equals("mobile")) {
- return MOBILE;
- } else if (s.equals("server")) {
- return SERVER;
- } else {
- return OTHER;
- }
- }
-
- @Override
- public String toString() {
- return super.toString().toLowerCase();
- }
-
- }
-
- public String getId() {
- return id;
- }
-
- public String getCaption() {
- return caption;
- }
-
- public DeviceType getType() {
- return type;
- }
-
- public int getSubscriptions() {
- return subscriptions;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java
deleted file mode 100644
index b002035c9..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package de.danoeh.antennapod.gpoddernet.model;
-
-import org.apache.commons.lang3.Validate;
-
-public class GpodnetPodcast {
- private String url;
- private String title;
- private String description;
- private int subscribers;
- private String logoUrl;
- private String website;
- private String mygpoLink;
-
- public GpodnetPodcast(String url, String title, String description,
- int subscribers, String logoUrl, String website, String mygpoLink) {
- Validate.notNull(url);
- Validate.notNull(title);
- Validate.notNull(description);
-
- this.url = url;
- this.title = title;
- this.description = description;
- this.subscribers = subscribers;
- this.logoUrl = logoUrl;
- this.website = website;
- this.mygpoLink = mygpoLink;
- }
-
- @Override
- public String toString() {
- return "GpodnetPodcast [url=" + url + ", title=" + title
- + ", description=" + description + ", subscribers="
- + subscribers + ", logoUrl=" + logoUrl + ", website=" + website
- + ", mygpoLink=" + mygpoLink + "]";
- }
-
- public String getUrl() {
- return url;
- }
-
- public String getTitle() {
- return title;
- }
-
- public String getDescription() {
- return description;
- }
-
- public int getSubscribers() {
- return subscribers;
- }
-
- public String getLogoUrl() {
- return logoUrl;
- }
-
- public String getWebsite() {
- return website;
- }
-
- public String getMygpoLink() {
- return mygpoLink;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java
deleted file mode 100644
index a4617118d..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package de.danoeh.antennapod.gpoddernet.model;
-
-import org.apache.commons.lang3.Validate;
-
-import java.util.List;
-
-public class GpodnetSubscriptionChange {
- private List<String> added;
- private List<String> removed;
- private long timestamp;
-
- public GpodnetSubscriptionChange(List<String> added, List<String> removed,
- long timestamp) {
- Validate.notNull(added);
- Validate.notNull(removed);
-
- this.added = added;
- this.removed = removed;
- this.timestamp = timestamp;
- }
-
- @Override
- public String toString() {
- return "GpodnetSubscriptionChange [added=" + added.toString()
- + ", removed=" + removed.toString() + ", timestamp="
- + timestamp + "]";
- }
-
- public List<String> getAdded() {
- return added;
- }
-
- public List<String> getRemoved() {
- return removed;
- }
-
- public long getTimestamp() {
- return timestamp;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java
deleted file mode 100644
index 80b84095e..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.danoeh.antennapod.gpoddernet.model;
-
-import org.apache.commons.lang3.Validate;
-
-import java.util.Comparator;
-
-public class GpodnetTag {
-
- private String name;
- private int usage;
-
- public GpodnetTag(String name, int usage) {
- Validate.notNull(name);
-
- this.name = name;
- this.usage = usage;
- }
-
- public GpodnetTag(String name) {
- super();
- this.name = name;
- }
-
- @Override
- public String toString() {
- return "GpodnetTag [name=" + name + ", usage=" + usage + "]";
- }
-
- public String getName() {
- return name;
- }
-
- public int getUsage() {
- return usage;
- }
-
- public static class UsageComparator implements Comparator<GpodnetTag> {
-
- @Override
- public int compare(GpodnetTag o1, GpodnetTag o2) {
- return o1.usage - o2.usage;
- }
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java
deleted file mode 100644
index fee8c7d28..000000000
--- a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package de.danoeh.antennapod.gpoddernet.model;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Object returned by {@link de.danoeh.antennapod.gpoddernet.GpodnetService} in uploadChanges method.
- */
-public class GpodnetUploadChangesResponse {
-
- /**
- * timestamp/ID that can be used for requesting changes since this upload.
- */
- public final long timestamp;
-
- /**
- * URLs that should be updated. The key of the map is the original URL, the value of the map
- * is the sanitized URL.
- */
- public final Map<String, String> updatedUrls;
-
- public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) {
- this.timestamp = timestamp;
- this.updatedUrls = updatedUrls;
- }
-
- /**
- * Creates a new GpodnetUploadChangesResponse-object from a JSON object that was
- * returned by an uploadChanges call.
- *
- * @throws org.json.JSONException If the method could not parse the JSONObject.
- */
- public static GpodnetUploadChangesResponse fromJSONObject(String objectString) throws JSONException {
- final JSONObject object = new JSONObject(objectString);
- final long timestamp = object.getLong("timestamp");
- Map<String, String> updatedUrls = new HashMap<String, String>();
- JSONArray urls = object.getJSONArray("update_urls");
- for (int i = 0; i < urls.length(); i++) {
- JSONArray urlPair = urls.getJSONArray(i);
- updatedUrls.put(urlPair.getString(0), urlPair.getString(1));
- }
- return new GpodnetUploadChangesResponse(timestamp, updatedUrls);
- }
-
- @Override
- public String toString() {
- return "GpodnetUploadChangesResponse{" +
- "timestamp=" + timestamp +
- ", updatedUrls=" + updatedUrls +
- '}';
- }
-}
diff --git a/src/de/danoeh/antennapod/opml/OpmlElement.java b/src/de/danoeh/antennapod/opml/OpmlElement.java
deleted file mode 100644
index 4cb563c04..000000000
--- a/src/de/danoeh/antennapod/opml/OpmlElement.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.danoeh.antennapod.opml;
-
-/** Represents a single feed in an OPML file. */
-public class OpmlElement {
- private String text;
- private String xmlUrl;
- private String htmlUrl;
- private String type;
-
- public OpmlElement() {
-
- }
-
- public String getText() {
- return text;
- }
-
- public void setText(String text) {
- this.text = text;
- }
-
- public String getXmlUrl() {
- return xmlUrl;
- }
-
- public void setXmlUrl(String xmlUrl) {
- this.xmlUrl = xmlUrl;
- }
-
- public String getHtmlUrl() {
- return htmlUrl;
- }
-
- public void setHtmlUrl(String htmlUrl) {
- this.htmlUrl = htmlUrl;
- }
-
- public String getType() {
- return type;
- }
-
- public void setType(String type) {
- this.type = type;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/opml/OpmlReader.java b/src/de/danoeh/antennapod/opml/OpmlReader.java
deleted file mode 100644
index 19a980dee..000000000
--- a/src/de/danoeh/antennapod/opml/OpmlReader.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package de.danoeh.antennapod.opml;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.IOException;
-import java.io.Reader;
-import java.util.ArrayList;
-
-/** Reads OPML documents. */
-public class OpmlReader {
- private static final String TAG = "OpmlReader";
-
- // ATTRIBUTES
- private boolean isInOpml = false;
- private ArrayList<OpmlElement> elementList;
-
- /**
- * Reads an Opml document and returns a list of all OPML elements it can
- * find
- *
- * @throws IOException
- * @throws XmlPullParserException
- */
- public ArrayList<OpmlElement> readDocument(Reader reader)
- throws XmlPullParserException, IOException {
- elementList = new ArrayList<OpmlElement>();
- XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
- factory.setNamespaceAware(true);
- XmlPullParser xpp = factory.newPullParser();
- xpp.setInput(reader);
- int eventType = xpp.getEventType();
-
- while (eventType != XmlPullParser.END_DOCUMENT) {
- switch (eventType) {
- case XmlPullParser.START_DOCUMENT:
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Reached beginning of document");
- break;
- case XmlPullParser.START_TAG:
- if (xpp.getName().equals(OpmlSymbols.OPML)) {
- isInOpml = true;
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Reached beginning of OPML tree.");
- } else if (isInOpml && xpp.getName().equals(OpmlSymbols.OUTLINE)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Found new Opml element");
- OpmlElement element = new OpmlElement();
-
- final String title = xpp.getAttributeValue(null, OpmlSymbols.TITLE);
- if (title != null) {
- Log.i(TAG, "Using title: " + title);
- element.setText(title);
- } else {
- Log.i(TAG, "Title not found, using text");
- element.setText(xpp.getAttributeValue(null, OpmlSymbols.TEXT));
- }
- element.setXmlUrl(xpp.getAttributeValue(null, OpmlSymbols.XMLURL));
- element.setHtmlUrl(xpp.getAttributeValue(null, OpmlSymbols.HTMLURL));
- element.setType(xpp.getAttributeValue(null, OpmlSymbols.TYPE));
- if (element.getXmlUrl() != null) {
- if (element.getText() == null) {
- Log.i(TAG, "Opml element has no text attribute.");
- element.setText(element.getXmlUrl());
- }
- elementList.add(element);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Skipping element because of missing xml url");
- }
- }
- break;
- }
- eventType = xpp.next();
- }
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Parsing finished.");
-
- return elementList;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/opml/OpmlSymbols.java b/src/de/danoeh/antennapod/opml/OpmlSymbols.java
deleted file mode 100644
index 4b0b7316a..000000000
--- a/src/de/danoeh/antennapod/opml/OpmlSymbols.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.danoeh.antennapod.opml;
-
-/** Contains symbols for reading and writing OPML documents. */
-public final class OpmlSymbols {
-
- public static final String OPML = "opml";
- public static final String BODY = "body";
- public static final String OUTLINE = "outline";
- public static final String TEXT = "text";
- public static final String XMLURL = "xmlUrl";
- public static final String HTMLURL = "htmlUrl";
- public static final String TYPE = "type";
- public static final String VERSION = "version";
- public static final String HEAD = "head";
- public static final String TITLE = "title";
-
- private OpmlSymbols() {
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/opml/OpmlWriter.java b/src/de/danoeh/antennapod/opml/OpmlWriter.java
deleted file mode 100644
index 405a5e35a..000000000
--- a/src/de/danoeh/antennapod/opml/OpmlWriter.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package de.danoeh.antennapod.opml;
-
-import android.util.Log;
-import android.util.Xml;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Feed;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-import java.io.Writer;
-import java.util.List;
-
-/** Writes OPML documents. */
-public class OpmlWriter {
- private static final String TAG = "OpmlWriter";
- private static final String ENCODING = "UTF-8";
- private static final String OPML_VERSION = "2.0";
- private static final String OPML_TITLE = "AntennaPod Subscriptions";
-
- /**
- * Takes a list of feeds and a writer and writes those into an OPML
- * document.
- *
- * @throws IOException
- * @throws IllegalStateException
- * @throws IllegalArgumentException
- */
- public void writeDocument(List<Feed> feeds, Writer writer)
- throws IllegalArgumentException, IllegalStateException, IOException {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Starting to write document");
- XmlSerializer xs = Xml.newSerializer();
- xs.setOutput(writer);
-
- xs.startDocument(ENCODING, false);
- xs.startTag(null, OpmlSymbols.OPML);
- xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION);
-
- xs.startTag(null, OpmlSymbols.HEAD);
- xs.startTag(null, OpmlSymbols.TITLE);
- xs.text(OPML_TITLE);
- xs.endTag(null, OpmlSymbols.TITLE);
- xs.endTag(null, OpmlSymbols.HEAD);
-
- xs.startTag(null, OpmlSymbols.BODY);
- for (Feed feed : feeds) {
- xs.startTag(null, OpmlSymbols.OUTLINE);
- xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle());
- xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle());
- if (feed.getType() != null) {
- xs.attribute(null, OpmlSymbols.TYPE, feed.getType());
- }
- xs.attribute(null, OpmlSymbols.XMLURL, feed.getDownload_url());
- if (feed.getLink() != null) {
- xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink());
- }
- xs.endTag(null, OpmlSymbols.OUTLINE);
- }
- xs.endTag(null, OpmlSymbols.BODY);
- xs.endTag(null, OpmlSymbols.OPML);
- xs.endDocument();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Finished writing document");
- }
-}
diff --git a/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java b/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java
deleted file mode 100644
index bdfe297a6..000000000
--- a/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java
+++ /dev/null
@@ -1,246 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.service.GpodnetSyncService;
-
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Manages preferences for accessing gpodder.net service
- */
-public class GpodnetPreferences {
-
- private static final String TAG = "GpodnetPreferences";
-
- private static final String PREF_NAME = "gpodder.net";
- public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username";
- public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password";
- public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
- public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname";
-
-
- public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
- public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added";
- public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed";
-
- private static String username;
- private static String password;
- private static String deviceID;
- private static String hostname;
-
- private static ReentrantLock feedListLock = new ReentrantLock();
- private static Set<String> addedFeeds;
- private static Set<String> removedFeeds;
-
- /**
- * Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
- */
- private static long lastSyncTimestamp;
-
- private static boolean preferencesLoaded = false;
-
- private static SharedPreferences getPreferences() {
- return PodcastApp.getInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- }
-
- private static synchronized void ensurePreferencesLoaded() {
- if (!preferencesLoaded) {
- SharedPreferences prefs = getPreferences();
- username = prefs.getString(PREF_GPODNET_USERNAME, null);
- password = prefs.getString(PREF_GPODNET_PASSWORD, null);
- deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
- lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0);
- addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, ""));
- removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, ""));
- hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST));
-
- preferencesLoaded = true;
- }
- }
-
- private static void writePreference(String key, String value) {
- SharedPreferences.Editor editor = getPreferences().edit();
- editor.putString(key, value);
- editor.commit();
- }
-
- private static void writePreference(String key, long value) {
- SharedPreferences.Editor editor = getPreferences().edit();
- editor.putLong(key, value);
- editor.commit();
- }
-
- private static void writePreference(String key, Collection<String> value) {
- SharedPreferences.Editor editor = getPreferences().edit();
- editor.putString(key, writeListToString(value));
- editor.commit();
- }
-
- public static String getUsername() {
- ensurePreferencesLoaded();
- return username;
- }
-
- public static void setUsername(String username) {
- GpodnetPreferences.username = username;
- writePreference(PREF_GPODNET_USERNAME, username);
- }
-
- public static String getPassword() {
- ensurePreferencesLoaded();
- return password;
- }
-
- public static void setPassword(String password) {
- GpodnetPreferences.password = password;
- writePreference(PREF_GPODNET_PASSWORD, password);
- }
-
- public static String getDeviceID() {
- ensurePreferencesLoaded();
- return deviceID;
- }
-
- public static void setDeviceID(String deviceID) {
- GpodnetPreferences.deviceID = deviceID;
- writePreference(PREF_GPODNET_DEVICEID, deviceID);
- }
-
- public static long getLastSyncTimestamp() {
- ensurePreferencesLoaded();
- return lastSyncTimestamp;
- }
-
- public static void setLastSyncTimestamp(long lastSyncTimestamp) {
- GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp;
- writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp);
- }
-
- public static String getHostname() {
- ensurePreferencesLoaded();
- return hostname;
- }
-
- public static void setHostname(String value) {
- value = checkGpodnetHostname(value);
- if (!value.equals(hostname)) {
- logout();
- writePreference(PREF_GPODNET_HOSTNAME, value);
- hostname = value;
- }
- }
-
- public static void addAddedFeed(String feed) {
- ensurePreferencesLoaded();
- feedListLock.lock();
- if (addedFeeds.add(feed)) {
- writePreference(PREF_SYNC_ADDED, addedFeeds);
- }
- if (removedFeeds.remove(feed)) {
- writePreference(PREF_SYNC_REMOVED, removedFeeds);
- }
- feedListLock.unlock();
- GpodnetSyncService.sendSyncIntent(PodcastApp.getInstance());
- }
-
- public static void addRemovedFeed(String feed) {
- ensurePreferencesLoaded();
- feedListLock.lock();
- if (removedFeeds.add(feed)) {
- writePreference(PREF_SYNC_REMOVED, removedFeeds);
- }
- if (addedFeeds.remove(feed)) {
- writePreference(PREF_SYNC_ADDED, addedFeeds);
- }
- feedListLock.unlock();
- GpodnetSyncService.sendSyncIntent(PodcastApp.getInstance());
- }
-
- public static Set<String> getAddedFeedsCopy() {
- ensurePreferencesLoaded();
- Set<String> copy = new HashSet<String>();
- feedListLock.lock();
- copy.addAll(addedFeeds);
- feedListLock.unlock();
- return copy;
- }
-
- public static void removeAddedFeeds(Collection<String> removed) {
- ensurePreferencesLoaded();
- feedListLock.lock();
- addedFeeds.removeAll(removed);
- writePreference(PREF_SYNC_ADDED, addedFeeds);
- feedListLock.unlock();
- }
-
- public static Set<String> getRemovedFeedsCopy() {
- ensurePreferencesLoaded();
- Set<String> copy = new HashSet<String>();
- feedListLock.lock();
- copy.addAll(removedFeeds);
- feedListLock.unlock();
- return copy;
- }
-
- public static void removeRemovedFeeds(Collection<String> removed) {
- ensurePreferencesLoaded();
- removedFeeds.removeAll(removed);
- writePreference(PREF_SYNC_REMOVED, removedFeeds);
-
- }
-
- /**
- * Returns true if device ID, username and password have a non-null value
- */
- public static boolean loggedIn() {
- ensurePreferencesLoaded();
- return deviceID != null && username != null && password != null;
- }
-
- public static synchronized void logout() {
- if (BuildConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences");
- setUsername(null);
- setPassword(null);
- setDeviceID(null);
- addedFeeds.clear();
- writePreference(PREF_SYNC_ADDED, addedFeeds);
- removedFeeds.clear();
- writePreference(PREF_SYNC_REMOVED, removedFeeds);
- setLastSyncTimestamp(0);
- }
-
- private static Set<String> readListFromString(String s) {
- Set<String> result = new HashSet<String>();
- for (String item : s.split(" ")) {
- result.add(item);
- }
- return result;
- }
-
- private static String writeListToString(Collection<String> c) {
- StringBuilder result = new StringBuilder();
- for (String item : c) {
- result.append(item);
- result.append(" ");
- }
- return result.toString().trim();
- }
-
- private static String checkGpodnetHostname(String value) {
- int startIndex = 0;
- if (value.startsWith("http://")) {
- startIndex = "http://".length();
- } else if (value.startsWith("https://")) {
- startIndex = "https://".length();
- }
- return value.substring(startIndex);
- }
-}
diff --git a/src/de/danoeh/antennapod/preferences/PlaybackPreferences.java b/src/de/danoeh/antennapod/preferences/PlaybackPreferences.java
deleted file mode 100644
index 1d1ab052f..000000000
--- a/src/de/danoeh/antennapod/preferences/PlaybackPreferences.java
+++ /dev/null
@@ -1,146 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.util.Log;
-
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.BuildConfig;
-
-/**
- * Provides access to preferences set by the playback service. A private
- * instance of this class must first be instantiated via createInstance() or
- * otherwise every public method will throw an Exception when called.
- */
-public class PlaybackPreferences implements
- SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "PlaybackPreferences";
-
- /**
- * Contains the feed id of the currently playing item if it is a FeedMedia
- * object.
- */
- public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId";
-
- /**
- * Contains the id of the currently playing FeedMedia object or
- * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object.
- */
- public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId";
-
- /**
- * Type of the media object that is currently being played. This preference
- * is set to NO_MEDIA_PLAYING after playback has been completed and is set
- * as soon as the 'play' button is pressed.
- */
- public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia";
-
- /** True if last played media was streamed. */
- public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
-
- /** True if last played media was a video. */
- public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo";
-
- /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */
- public static final long NO_MEDIA_PLAYING = -1;
-
- private long currentlyPlayingFeedId;
- private long currentlyPlayingFeedMediaId;
- private long currentlyPlayingMedia;
- private boolean currentEpisodeIsStream;
- private boolean currentEpisodeIsVideo;
-
- private static PlaybackPreferences instance;
- private Context context;
-
- private PlaybackPreferences(Context context) {
- this.context = context;
- loadPreferences();
- }
-
- /**
- * Sets up the UserPreferences class.
- *
- * @throws IllegalArgumentException
- * if context is null
- * */
- public static void createInstance(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Creating new instance of UserPreferences");
- Validate.notNull(context);
-
- instance = new PlaybackPreferences(context);
-
- PreferenceManager.getDefaultSharedPreferences(context)
- .registerOnSharedPreferenceChangeListener(instance);
- }
-
- private void loadPreferences() {
- SharedPreferences sp = PreferenceManager
- .getDefaultSharedPreferences(context);
- currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1);
- currentlyPlayingFeedMediaId = sp.getLong(
- PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
- currentlyPlayingMedia = sp.getLong(PREF_CURRENTLY_PLAYING_MEDIA,
- NO_MEDIA_PLAYING);
- currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
- currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
- if (key.equals(PREF_CURRENTLY_PLAYING_FEED_ID)) {
- currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID,
- -1);
-
- } else if (key.equals(PREF_CURRENTLY_PLAYING_MEDIA)) {
- currentlyPlayingMedia = sp
- .getLong(PREF_CURRENTLY_PLAYING_MEDIA, -1);
-
- } else if (key.equals(PREF_CURRENT_EPISODE_IS_STREAM)) {
- currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
-
- } else if (key.equals(PREF_CURRENT_EPISODE_IS_VIDEO)) {
- currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
-
- } else if (key.equals(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID)) {
- currentlyPlayingFeedMediaId = sp.getLong(
- PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
- }
- }
-
- private static void instanceAvailable() {
- if (instance == null) {
- throw new IllegalStateException(
- "UserPreferences was used before being set up");
- }
- }
-
-
- public static long getLastPlayedFeedId() {
- instanceAvailable();
- return instance.currentlyPlayingFeedId;
- }
-
- public static long getCurrentlyPlayingMedia() {
- instanceAvailable();
- return instance.currentlyPlayingMedia;
- }
-
- public static long getCurrentlyPlayingFeedMediaId() {
- return instance.currentlyPlayingFeedMediaId;
- }
-
- public static boolean getCurrentEpisodeIsStream() {
- instanceAvailable();
- return instance.currentEpisodeIsStream;
- }
-
- public static boolean getCurrentEpisodeIsVideo() {
- instanceAvailable();
- return instance.currentEpisodeIsVideo;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java
deleted file mode 100644
index 73a4a1a14..000000000
--- a/src/de/danoeh/antennapod/preferences/UserPreferences.java
+++ /dev/null
@@ -1,609 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-import org.json.JSONArray;
-import org.json.JSONException;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.OpmlImportFromPathActivity;
-import de.danoeh.antennapod.receiver.FeedUpdateReceiver;
-
-/**
- * Provides access to preferences set by the user in the settings screen. A
- * private instance of this class must first be instantiated via
- * createInstance() or otherwise every public method will throw an Exception
- * when called.
- */
-public class UserPreferences implements
- SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "UserPreferences";
-
- public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect";
- public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue";
- public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly";
- public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall";
- public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate";
- public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes";
- public static final String PREF_AUTO_DELETE = "prefAutoDelete";
- public static final String PREF_AUTO_FLATTR = "pref_auto_flattr";
- public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold";
- public static final String PREF_THEME = "prefTheme";
- public static final String PREF_DATA_FOLDER = "prefDataFolder";
- public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl";
- public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter";
- private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks";
- public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize";
- private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed";
- private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
- public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss";
- private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs";
- private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
- private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify";
-
- // TODO: Make this value configurable
- private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f;
-
- private static int EPISODE_CACHE_SIZE_UNLIMITED = -1;
-
- private static UserPreferences instance;
- private final Context context;
-
- // Preferences
- private boolean pauseOnHeadsetDisconnect;
- private boolean followQueue;
- private boolean downloadMediaOnWifiOnly;
- private long updateInterval;
- private boolean allowMobileUpdate;
- private boolean displayOnlyEpisodes;
- private boolean autoDelete;
- private boolean autoFlattr;
- private float autoFlattrPlayedDurationThreshold;
- private int theme;
- private boolean enableAutodownload;
- private boolean enableAutodownloadWifiFilter;
- private String[] autodownloadSelectedNetworks;
- private int episodeCacheSize;
- private String playbackSpeed;
- private String[] playbackSpeedArray;
- private boolean pauseForFocusLoss;
- private int seekDeltaSecs;
- private boolean isFreshInstall;
- private int notifyPriority;
- private boolean persistNotify;
-
- private UserPreferences(Context context) {
- this.context = context;
- loadPreferences();
- }
-
- /**
- * Sets up the UserPreferences class.
- *
- * @throws IllegalArgumentException if context is null
- */
- public static void createInstance(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Creating new instance of UserPreferences");
- Validate.notNull(context);
-
- instance = new UserPreferences(context);
-
- createImportDirectory();
- createNoMediaFile();
- PreferenceManager.getDefaultSharedPreferences(context)
- .registerOnSharedPreferenceChangeListener(instance);
-
- }
-
- private void loadPreferences() {
- SharedPreferences sp = PreferenceManager
- .getDefaultSharedPreferences(context);
- EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger(
- R.integer.episode_cache_size_unlimited);
- pauseOnHeadsetDisconnect = sp.getBoolean(
- PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
- followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false);
- downloadMediaOnWifiOnly = sp.getBoolean(
- PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true);
- updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL,
- "0"));
- allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false);
- displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false);
- autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false);
- autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
- autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
- PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
- theme = readThemeValue(sp.getString(PREF_THEME, "0"));
- enableAutodownloadWifiFilter = sp.getBoolean(
- PREF_ENABLE_AUTODL_WIFI_FILTER, false);
- autodownloadSelectedNetworks = StringUtils.split(
- sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ',');
- episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(
- PREF_EPISODE_CACHE_SIZE, "20"));
- enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
- playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
- playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
- PREF_PLAYBACK_SPEED_ARRAY, null));
- pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
- seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30"));
- if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
- notifyPriority = NotificationCompat.PRIORITY_MAX;
- }
- else {
- notifyPriority = NotificationCompat.PRIORITY_DEFAULT;
- }
- persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false);
- }
-
- private int readThemeValue(String valueFromPrefs) {
- switch (Integer.parseInt(valueFromPrefs)) {
- case 0:
- return R.style.Theme_AntennaPod_Light;
- case 1:
- return R.style.Theme_AntennaPod_Dark;
- default:
- return R.style.Theme_AntennaPod_Light;
- }
- }
-
- private long readUpdateInterval(String valueFromPrefs) {
- int hours = Integer.parseInt(valueFromPrefs);
- return TimeUnit.HOURS.toMillis(hours);
- }
-
- private int readEpisodeCacheSizeInternal(String valueFromPrefs) {
- if (valueFromPrefs.equals(context
- .getString(R.string.pref_episode_cache_unlimited))) {
- return EPISODE_CACHE_SIZE_UNLIMITED;
- } else {
- return Integer.valueOf(valueFromPrefs);
- }
- }
-
- private String[] readPlaybackSpeedArray(String valueFromPrefs) {
- String[] selectedSpeeds = null;
- // If this preference hasn't been set yet, return the default options
- if (valueFromPrefs == null) {
- String[] allSpeeds = context.getResources().getStringArray(
- R.array.playback_speed_values);
- List<String> speedList = new LinkedList<String>();
- for (String speedStr : allSpeeds) {
- float speed = Float.parseFloat(speedStr);
- if (speed < 2.0001 && speed * 10 % 1 == 0) {
- speedList.add(speedStr);
- }
- }
- selectedSpeeds = speedList.toArray(new String[speedList.size()]);
- } else {
- try {
- JSONArray jsonArray = new JSONArray(valueFromPrefs);
- selectedSpeeds = new String[jsonArray.length()];
- for (int i = 0; i < jsonArray.length(); i++) {
- selectedSpeeds[i] = jsonArray.getString(i);
- }
- } catch (JSONException e) {
- Log.e(TAG,
- "Got JSON error when trying to get speeds from JSONArray");
- e.printStackTrace();
- }
- }
- return selectedSpeeds;
- }
-
- private static void instanceAvailable() {
- if (instance == null) {
- throw new IllegalStateException(
- "UserPreferences was used before being set up");
- }
- }
-
- public static boolean isPauseOnHeadsetDisconnect() {
- instanceAvailable();
- return instance.pauseOnHeadsetDisconnect;
- }
-
- public static boolean isFollowQueue() {
- instanceAvailable();
- return instance.followQueue;
- }
-
- public static boolean isDownloadMediaOnWifiOnly() {
- instanceAvailable();
- return instance.downloadMediaOnWifiOnly;
- }
-
- public static long getUpdateInterval() {
- instanceAvailable();
- return instance.updateInterval;
- }
-
- public static boolean isAllowMobileUpdate() {
- instanceAvailable();
- return instance.allowMobileUpdate;
- }
-
- public static boolean isDisplayOnlyEpisodes() {
- instanceAvailable();
- //return instance.displayOnlyEpisodes;
- return false;
- }
-
- public static boolean isAutoDelete() {
- instanceAvailable();
- return instance.autoDelete;
- }
-
- public static boolean isAutoFlattr() {
- instanceAvailable();
- return instance.autoFlattr;
- }
-
- public static int getNotifyPriority() {
- instanceAvailable();
- return instance.notifyPriority;
- }
-
- public static boolean isPersistNotify() {
- instanceAvailable();
- return instance.persistNotify;
- }
-
-
- /**
- * Returns the time after which an episode should be auto-flattr'd in percent of the episode's
- * duration.
- */
- public static float getAutoFlattrPlayedDurationThreshold() {
- instanceAvailable();
- return instance.autoFlattrPlayedDurationThreshold;
- }
-
- public static int getTheme() {
- instanceAvailable();
- return instance.theme;
- }
-
- public static boolean isEnableAutodownloadWifiFilter() {
- instanceAvailable();
- return instance.enableAutodownloadWifiFilter;
- }
-
- public static String[] getAutodownloadSelectedNetworks() {
- instanceAvailable();
- return instance.autodownloadSelectedNetworks;
- }
-
- public static int getEpisodeCacheSizeUnlimited() {
- return EPISODE_CACHE_SIZE_UNLIMITED;
- }
-
- public static String getPlaybackSpeed() {
- instanceAvailable();
- return instance.playbackSpeed;
- }
-
- public static String[] getPlaybackSpeedArray() {
- instanceAvailable();
- return instance.playbackSpeedArray;
- }
-
- public static int getSeekDeltaMs() {
- instanceAvailable();
- return 1000 * instance.seekDeltaSecs;
- }
-
- /**
- * Returns the capacity of the episode cache. This method will return the
- * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
- * 'unlimited'.
- */
- public static int getEpisodeCacheSize() {
- instanceAvailable();
- return instance.episodeCacheSize;
- }
-
- public static boolean isEnableAutodownload() {
- instanceAvailable();
- return instance.enableAutodownload;
- }
-
- public static boolean shouldPauseForFocusLoss() {
- instanceAvailable();
- return instance.pauseForFocusLoss;
- }
-
- public static boolean isFreshInstall() {
- instanceAvailable();
- return instance.isFreshInstall;
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Registered change of user preferences. Key: " + key);
-
- if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) {
- downloadMediaOnWifiOnly = sp.getBoolean(
- PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true);
-
- } else if (key.equals(PREF_MOBILE_UPDATE)) {
- allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false);
-
- } else if (key.equals(PREF_FOLLOW_QUEUE)) {
- followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false);
-
- } else if (key.equals(PREF_UPDATE_INTERVAL)) {
- updateInterval = readUpdateInterval(sp.getString(
- PREF_UPDATE_INTERVAL, "0"));
- restartUpdateAlarm(updateInterval);
-
- } else if (key.equals(PREF_AUTO_DELETE)) {
- autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false);
-
- } else if (key.equals(PREF_AUTO_FLATTR)) {
- autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
- } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) {
- displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES,
- false);
- } else if (key.equals(PREF_THEME)) {
- theme = readThemeValue(sp.getString(PREF_THEME, ""));
- } else if (key.equals(PREF_ENABLE_AUTODL_WIFI_FILTER)) {
- enableAutodownloadWifiFilter = sp.getBoolean(
- PREF_ENABLE_AUTODL_WIFI_FILTER, false);
- } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) {
- autodownloadSelectedNetworks = StringUtils.split(
- sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ',');
- } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) {
- episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(
- PREF_EPISODE_CACHE_SIZE, "20"));
- } else if (key.equals(PREF_ENABLE_AUTODL)) {
- enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false);
- } else if (key.equals(PREF_PLAYBACK_SPEED)) {
- playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0");
- } else if (key.equals(PREF_PLAYBACK_SPEED_ARRAY)) {
- playbackSpeedArray = readPlaybackSpeedArray(sp.getString(
- PREF_PLAYBACK_SPEED_ARRAY, null));
- } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) {
- pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
- } else if (key.equals(PREF_SEEK_DELTA_SECS)) {
- seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30"));
- } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) {
- pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true);
- } else if (key.equals(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD)) {
- autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
- PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
- } else if (key.equals(PREF_EXPANDED_NOTIFICATION)) {
- if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
- notifyPriority = NotificationCompat.PRIORITY_MAX;
- }
- else {
- notifyPriority = NotificationCompat.PRIORITY_DEFAULT;
- }
- } else if (key.equals(PREF_PERSISTENT_NOTIFICATION)) {
- persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false);
- }
- }
-
- public static void setPlaybackSpeed(String speed) {
- PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
- .putString(PREF_PLAYBACK_SPEED, speed).apply();
- }
-
- public static void setPlaybackSpeedArray(String[] speeds) {
- JSONArray jsonArray = new JSONArray();
- for (String speed : speeds) {
- jsonArray.put(speed);
- }
- PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
- .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString())
- .apply();
- }
-
- public static void setAutodownloadSelectedNetworks(Context context,
- String[] value) {
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(context.getApplicationContext())
- .edit();
- editor.putString(PREF_AUTODL_SELECTED_NETWORKS,
- StringUtils.join(value, ','));
- editor.commit();
- }
-
- /**
- * Sets the update interval value. Should only be used for testing purposes!
- */
- public static void setUpdateInterval(Context context, long newValue) {
- instanceAvailable();
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(context.getApplicationContext())
- .edit();
- editor.putString(PREF_UPDATE_INTERVAL,
- String.valueOf(newValue));
- editor.commit();
- instance.updateInterval = newValue;
- }
-
- /**
- * Change the auto-flattr settings
- *
- * @param context For accessing the shared preferences
- * @param enabled Whether automatic flattring should be enabled at all
- * @param autoFlattrThreshold The percentage of playback time after which an episode should be
- * flattrd. Must be a value between 0 and 1 (inclusive)
- * */
- public static void setAutoFlattrSettings(Context context, boolean enabled, float autoFlattrThreshold) {
- instanceAvailable();
- Validate.inclusiveBetween(0.0, 1.0, autoFlattrThreshold);
- PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext())
- .edit()
- .putBoolean(PREF_AUTO_FLATTR, enabled)
- .putFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, autoFlattrThreshold)
- .commit();
- instance.autoFlattr = enabled;
- instance.autoFlattrPlayedDurationThreshold = autoFlattrThreshold;
- }
-
- /**
- * Return the folder where the app stores all of its data. This method will
- * return the standard data folder if none has been set by the user.
- *
- * @param type The name of the folder inside the data folder. May be null
- * when accessing the root of the data folder.
- * @return The data folder that has been requested or null if the folder
- * could not be created.
- */
- public static File getDataFolder(Context context, String type) {
- instanceAvailable();
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(context.getApplicationContext());
- String strDir = prefs.getString(PREF_DATA_FOLDER, null);
- if (strDir == null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Using default data folder");
- return context.getExternalFilesDir(type);
- } else {
- File dataDir = new File(strDir);
- if (!dataDir.exists()) {
- if (!dataDir.mkdir()) {
- Log.w(TAG, "Could not create data folder");
- return null;
- }
- }
-
- if (type == null) {
- return dataDir;
- } else {
- // handle path separators
- String[] dirs = type.split("/");
- for (int i = 0; i < dirs.length; i++) {
- if (dirs.length > 0) {
- if (i < dirs.length - 1) {
- dataDir = getDataFolder(context, dirs[i]);
- if (dataDir == null) {
- return null;
- }
- }
- type = dirs[i];
- }
- }
- File typeDir = new File(dataDir, type);
- if (!typeDir.exists()) {
- if (dataDir.canWrite()) {
- if (!typeDir.mkdir()) {
- Log.e(TAG, "Could not create data folder named "
- + type);
- return null;
- }
- }
- }
- return typeDir;
- }
-
- }
- }
-
- public static void setDataFolder(String dir) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Result from DirectoryChooser: " + dir);
- instanceAvailable();
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(instance.context);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(PREF_DATA_FOLDER, dir);
- editor.commit();
- createImportDirectory();
- }
-
- /**
- * Create a .nomedia file to prevent scanning by the media scanner.
- */
- private static void createNoMediaFile() {
- File f = new File(instance.context.getExternalFilesDir(null),
- ".nomedia");
- if (!f.exists()) {
- try {
- f.createNewFile();
- } catch (IOException e) {
- Log.e(TAG, "Could not create .nomedia file");
- e.printStackTrace();
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, ".nomedia file created");
- }
- }
-
- /**
- * Creates the import directory if it doesn't exist and if storage is
- * available
- */
- private static void createImportDirectory() {
- File importDir = getDataFolder(instance.context,
- OpmlImportFromPathActivity.IMPORT_DIR);
- if (importDir != null) {
- if (importDir.exists()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Import directory already exists");
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Creating import directory");
- importDir.mkdir();
- }
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Could not access external storage.");
- }
- }
-
- /**
- * Updates alarm registered with the AlarmManager service or deactivates it.
- *
- * @param millis new value to register with AlarmManager. If millis is 0, the
- * alarm is deactivated.
- */
- public static void restartUpdateAlarm(long millis) {
- instanceAvailable();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Restarting update alarm. New value: " + millis);
- AlarmManager alarmManager = (AlarmManager) instance.context
- .getSystemService(Context.ALARM_SERVICE);
- PendingIntent updateIntent = PendingIntent.getBroadcast(
- instance.context, 0, new Intent(
- FeedUpdateReceiver.ACTION_REFRESH_FEEDS), 0
- );
- alarmManager.cancel(updateIntent);
- if (millis != 0) {
- alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, millis, millis,
- updateIntent);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Changed alarm to new interval");
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Automatic update was deactivated");
- }
- }
-
- /**
- * Reads episode cache size as it is saved in the episode_cache_size_values array.
- */
- public static int readEpisodeCacheSize(String valueFromPrefs) {
- instanceAvailable();
- return instance.readEpisodeCacheSizeInternal(valueFromPrefs);
- }
-}
diff --git a/src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java b/src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java
deleted file mode 100644
index a0539e276..000000000
--- a/src/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.danoeh.antennapod.receiver;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import org.apache.commons.lang3.StringUtils;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.preferences.UserPreferences;
-
-/** Listens for events that make it necessary to reset the update alarm. */
-public class AlarmUpdateReceiver extends BroadcastReceiver {
- private static final String TAG = "AlarmUpdateReceiver";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received intent");
- if (StringUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Resetting update alarm after reboot");
- } else if (StringUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Resetting update alarm after app upgrade");
- }
-
- UserPreferences.restartUpdateAlarm(UserPreferences.getUpdateInterval());
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
deleted file mode 100644
index 4dcf0b6aa..000000000
--- a/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
+++ /dev/null
@@ -1,46 +0,0 @@
-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.BuildConfig;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java b/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java
deleted file mode 100644
index 3c283a30b..000000000
--- a/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java
+++ /dev/null
@@ -1,46 +0,0 @@
-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.BuildConfig;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBTasks;
-
-/** Refreshes all feeds when it receives an intent */
-public class FeedUpdateReceiver extends BroadcastReceiver {
- private static final String TAG = "FeedUpdateReceiver";
- public static final String ACTION_REFRESH_FEEDS = "de.danoeh.antennapod.feedupdatereceiver.refreshFeeds";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_REFRESH_FEEDS)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received intent");
- boolean mobileUpdate = UserPreferences.isAllowMobileUpdate();
- if (mobileUpdate || connectedToWifi(context)) {
- DBTasks.refreshExpiredFeeds(context);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Blocking automatic update: no wifi available / no mobile updates allowed");
- }
- }
- }
-
- private boolean connectedToWifi(Context context) {
- ConnectivityManager connManager = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo mWifi = connManager
- .getNetworkInfo(ConnectivityManager.TYPE_WIFI);
-
- return mWifi.isConnected();
- }
-
-}
diff --git a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
deleted file mode 100644
index 1edebd275..000000000
--- a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package de.danoeh.antennapod.receiver;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-import android.view.KeyEvent;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-
-/** Receives media button events. */
-public class MediaButtonReceiver extends BroadcastReceiver {
- private static final String TAG = "MediaButtonReceiver";
- public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.service.extra.MediaButtonReceiver.KEYCODE";
-
- public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Received intent");
- KeyEvent event = (KeyEvent) intent.getExtras().get(
- Intent.EXTRA_KEY_EVENT);
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- Intent serviceIntent = new Intent(context, PlaybackService.class);
- int keycode = event.getKeyCode();
- serviceIntent.putExtra(EXTRA_KEYCODE, keycode);
- context.startService(serviceIntent);
- }
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/receiver/PlayerWidget.java b/src/de/danoeh/antennapod/receiver/PlayerWidget.java
deleted file mode 100644
index 9f8892181..000000000
--- a/src/de/danoeh/antennapod/receiver/PlayerWidget.java
+++ /dev/null
@@ -1,50 +0,0 @@
-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.BuildConfig;
-import de.danoeh.antennapod.service.playback.PlayerWidgetService;
-
-public class PlayerWidget extends AppWidgetProvider {
- private static final String TAG = "PlayerWidget";
- public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE";
- public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), FORCE_WIDGET_UPDATE)) {
- startUpdate(context);
- } else if (StringUtils.equals(intent.getAction(), 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/src/de/danoeh/antennapod/receiver/SPAReceiver.java b/src/de/danoeh/antennapod/receiver/SPAReceiver.java
deleted file mode 100644
index b0430d170..000000000
--- a/src/de/danoeh/antennapod/receiver/SPAReceiver.java
+++ /dev/null
@@ -1,55 +0,0 @@
-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.feed.Feed;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/service/GpodnetSyncService.java b/src/de/danoeh/antennapod/service/GpodnetSyncService.java
deleted file mode 100644
index c8c9fc31e..000000000
--- a/src/de/danoeh/antennapod/service/GpodnetSyncService.java
+++ /dev/null
@@ -1,245 +0,0 @@
-package de.danoeh.antennapod.service;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceAuthenticationException;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse;
-import de.danoeh.antennapod.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.util.NetworkUtils;
-
-import java.util.Date;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument.
- * This class also provides static methods for starting the GpodnetSyncService.
- */
-public class GpodnetSyncService extends Service {
- private static final String TAG = "GpodnetSyncService";
-
- private static final long WAIT_INTERVAL = 5000L;
-
- public static final String ARG_ACTION = "action";
-
- public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync";
-
- private GpodnetService service;
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null;
- if (action != null && action.equals(ACTION_SYNC)) {
- Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
- syncWaiterThread.restart();
- } else {
- Log.e(TAG, "Received invalid intent: action argument is null or invalid");
- }
- return START_FLAG_REDELIVERY;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy");
- syncWaiterThread.interrupt();
-
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- private synchronized GpodnetService tryLogin() throws GpodnetServiceException {
- if (service == null) {
- service = new GpodnetService();
- service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
- }
- return service;
- }
-
- private synchronized void syncChanges() {
- if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) {
- final long timestamp = GpodnetPreferences.getLastSyncTimestamp();
- try {
- final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this);
- GpodnetService service = tryLogin();
-
- if (timestamp == 0) {
- // first sync: download all subscriptions...
- GpodnetSubscriptionChange changes =
- service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0);
- if (BuildConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + changes);
- processSubscriptionChanges(localSubscriptions, changes);
-
- // ... then upload all local subscriptions
- if (BuildConfig.DEBUG) Log.d(TAG, "Uploading subscription list: " + localSubscriptions);
- GpodnetUploadChangesResponse uploadChangesResponse =
- service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>());
- if (BuildConfig.DEBUG) Log.d(TAG, "Uploading changes response: " + uploadChangesResponse);
- GpodnetPreferences.removeAddedFeeds(localSubscriptions);
- GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy());
- GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
- } else {
- Set<String> added = GpodnetPreferences.getAddedFeedsCopy();
- Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy();
-
- // download remote changes first...
- GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp);
- if (BuildConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
- processSubscriptionChanges(localSubscriptions, subscriptionChanges);
-
- // ... then upload changes local changes
- if (BuildConfig.DEBUG) Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
- added.toString(), removed));
- GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed);
- if (BuildConfig.DEBUG) Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse);
-
- GpodnetPreferences.removeAddedFeeds(added);
- GpodnetPreferences.removeRemovedFeeds(removed);
- GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
- }
- clearErrorNotifications();
- } catch (GpodnetServiceException e) {
- e.printStackTrace();
- updateErrorNotification(e);
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- }
- }
- stopSelf();
- }
-
- private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException {
- for (String downloadUrl : changes.getAdded()) {
- if (!localSubscriptions.contains(downloadUrl)) {
- Feed feed = new Feed(downloadUrl, new Date());
- DownloadRequester.getInstance().downloadFeed(this, feed);
- }
- }
- for (String downloadUrl : changes.getRemoved()) {
- DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl);
- }
- }
-
- private void clearErrorNotifications() {
- NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- nm.cancel(R.id.notification_gpodnet_sync_error);
- nm.cancel(R.id.notification_gpodnet_sync_autherror);
- }
-
- private void updateErrorNotification(GpodnetServiceException exception) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification");
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- final String title;
- final String description;
- final int id;
- if (exception instanceof GpodnetServiceAuthenticationException) {
- title = getString(R.string.gpodnetsync_auth_error_title);
- description = getString(R.string.gpodnetsync_auth_error_descr);
- id = R.id.notification_gpodnet_sync_autherror;
- } else {
- title = getString(R.string.gpodnetsync_error_title);
- description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage();
- id = R.id.notification_gpodnet_sync_error;
- }
-
- PendingIntent activityIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
-
- Notification notification = builder.setContentTitle(title)
- .setContentText(description)
- .setContentIntent(activityIntent)
- .setSmallIcon(R.drawable.stat_notify_sync_error)
- .setAutoCancel(true)
- .build();
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(id, notification);
- }
-
- private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) {
- @Override
- public void onWaitCompleted() {
- syncChanges();
- }
- };
-
- private abstract class WaiterThread {
- private long waitInterval;
- private Thread thread;
-
- private WaiterThread(long waitInterval) {
- this.waitInterval = waitInterval;
- reinit();
- }
-
- public abstract void onWaitCompleted();
-
- public void exec() {
- if (!thread.isAlive()) {
- thread.start();
- }
- }
-
- private void reinit() {
- if (thread != null && thread.isAlive()) {
- Log.d(TAG, "Interrupting waiter thread");
- thread.interrupt();
- }
- thread = new Thread() {
- @Override
- public void run() {
- try {
- Thread.sleep(waitInterval);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- if (!isInterrupted()) {
- synchronized (this) {
- onWaitCompleted();
- }
- }
- }
- };
- }
-
- public void restart() {
- reinit();
- exec();
- }
-
- public void interrupt() {
- if (thread != null && thread.isAlive()) {
- thread.interrupt();
- }
- }
- }
-
- public static void sendSyncIntent(Context context) {
- if (GpodnetPreferences.loggedIn()) {
- Intent intent = new Intent(context, GpodnetSyncService.class);
- intent.putExtra(ARG_ACTION, ACTION_SYNC);
- context.startService(intent);
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/service/download/APRedirectHandler.java b/src/de/danoeh/antennapod/service/download/APRedirectHandler.java
deleted file mode 100644
index ddf8d605d..000000000
--- a/src/de/danoeh/antennapod/service/download/APRedirectHandler.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.impl.client.DefaultRedirectHandler;
-import org.apache.http.protocol.HttpContext;
-
-import java.net.URI;
-
-public class APRedirectHandler extends DefaultRedirectHandler {
- // Identifier for logger
- private static final String TAG = "APRedirectHandler";
- // Header field, which has to be potentially fixed
- private static final String LOC = "Location";
- // Regular expressions for character strings, which should not appear in URLs
- private static final String CHi[] = { "\\{", "\\}", "\\|", "\\\\", "\\^", "~", "\\[", "\\]", "\\`"};
- private static final String CHo[] = { "%7B", "%7D", "%7C", "%5C", "%5E", "%7E", "%5B", "%5D", "%60"};
-
- /**
- * Workaround for broken URLs in redirection.
- * Proper solution involves LaxRedirectStrategy() which is not available in
- * current API yet.
- */
- @Override
- public URI getLocationURI(HttpResponse response, HttpContext context)
- throws org.apache.http.ProtocolException {
-
- Header h[] = response.getHeaders(LOC);
- if (h.length>0) {
- String s = h[0].getValue();
-
- // Fix broken URL
- for(int i=0; i<CHi.length;i++)
- s = s.replaceAll(CHi[i], CHo[i]);
-
- // If anything had to be fixed, then replace the header
- if (!s.equals(h[0].getValue()))
- {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Original URL: " + h[0].getValue());
-
- response.setHeader(LOC, s);
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Fixed URL: " + s);
- }
- }
-
- // call DefaultRedirectHandler with fixed URL
- return super.getLocationURI(response, context);
- }
-}
diff --git a/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java
deleted file mode 100644
index be331ce9b..000000000
--- a/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-import android.util.Log;
-import de.danoeh.antennapod.AppConfig;
-import de.danoeh.antennapod.BuildConfig;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.params.HttpClientParams;
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.conn.params.ConnManagerPNames;
-import org.apache.http.conn.scheme.PlainSocketFactory;
-import org.apache.http.conn.scheme.Scheme;
-import org.apache.http.conn.scheme.SchemeRegistry;
-import org.apache.http.conn.ssl.SSLSocketFactory;
-import org.apache.http.impl.client.AbstractHttpClient;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
-import org.apache.http.params.BasicHttpParams;
-import org.apache.http.params.CoreProtocolPNames;
-import org.apache.http.params.HttpConnectionParams;
-import org.apache.http.params.HttpParams;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * Provides access to a HttpClient singleton.
- */
-public class AntennapodHttpClient {
- private static final String TAG = "AntennapodHttpClient";
-
- public static final long EXPIRED_CONN_TIMEOUT_SEC = 30;
-
- public static final int MAX_REDIRECTS = 5;
- public static final int CONNECTION_TIMEOUT = 30000;
- public static final int SOCKET_TIMEOUT = 30000;
-
- public static final int MAX_CONNECTIONS = 8;
-
-
- private static volatile HttpClient httpClient = null;
-
- /**
- * Returns the HttpClient singleton.
- */
- public static synchronized HttpClient getHttpClient() {
- if (httpClient == null) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client");
-
- HttpParams params = new BasicHttpParams();
- params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT);
- params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS);
- params.setBooleanParameter("http.protocol.reject-relative-redirect",
- false);
- HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
- HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
- HttpClientParams.setRedirecting(params, true);
-
- httpClient = new DefaultHttpClient(createClientConnectionManager(), params);
- // Workaround for broken URLs in redirection
- ((AbstractHttpClient) httpClient)
- .setRedirectHandler(new APRedirectHandler());
- }
- return httpClient;
- }
-
- /**
- * Closes expired connections. This method should be called by the using class once has finished its work with
- * the HTTP client.
- */
- public static synchronized void cleanup() {
- if (httpClient != null) {
- httpClient.getConnectionManager().closeExpiredConnections();
- httpClient.getConnectionManager().closeIdleConnections(EXPIRED_CONN_TIMEOUT_SEC, TimeUnit.SECONDS);
- }
- }
-
-
- private static ClientConnectionManager createClientConnectionManager() {
- HttpParams params = new BasicHttpParams();
- params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, MAX_CONNECTIONS);
- return new ThreadSafeClientConnManager(params, prepareSchemeRegistry());
- }
-
- private static SchemeRegistry prepareSchemeRegistry() {
- SchemeRegistry sr = new SchemeRegistry();
-
- Scheme http = new Scheme("http",
- PlainSocketFactory.getSocketFactory(), 80);
- sr.register(http);
- Scheme https = new Scheme("https",
- SSLSocketFactory.getSocketFactory(), 443);
- sr.register(https);
-
- return sr;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/service/download/DownloadRequest.java b/src/de/danoeh/antennapod/service/download/DownloadRequest.java
deleted file mode 100644
index e803d30d4..000000000
--- a/src/de/danoeh/antennapod/service/download/DownloadRequest.java
+++ /dev/null
@@ -1,209 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import org.apache.commons.lang3.Validate;
-
-public class DownloadRequest implements Parcelable {
-
- private final String destination;
- private final String source;
- private final String title;
- private String username;
- private String password;
- private boolean deleteOnFailure;
- private final long feedfileId;
- private final int feedfileType;
-
- protected int progressPercent;
- protected long soFar;
- protected long size;
- protected int statusMsg;
-
- public DownloadRequest(String destination, String source, String title,
- long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure) {
- Validate.notNull(destination);
- Validate.notNull(source);
- Validate.notNull(title);
-
- this.destination = destination;
- this.source = source;
- this.title = title;
- this.feedfileId = feedfileId;
- this.feedfileType = feedfileType;
- this.username = username;
- this.password = password;
- this.deleteOnFailure = deleteOnFailure;
- }
-
- public DownloadRequest(String destination, String source, String title,
- long feedfileId, int feedfileType) {
- this(destination, source, title, feedfileId, feedfileType, null, null, true);
- }
-
- private DownloadRequest(Parcel in) {
- destination = in.readString();
- source = in.readString();
- title = in.readString();
- feedfileId = in.readLong();
- feedfileType = in.readInt();
- deleteOnFailure = (in.readByte() > 0);
- if (in.dataAvail() > 0) {
- username = in.readString();
- } else {
- username = null;
- }
- if (in.dataAvail() > 0) {
- password = in.readString();
- } else {
- password = null;
- }
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(destination);
- dest.writeString(source);
- dest.writeString(title);
- dest.writeLong(feedfileId);
- dest.writeInt(feedfileType);
- dest.writeByte((deleteOnFailure) ? (byte) 1 : 0);
- if (username != null) {
- dest.writeString(username);
- }
- if (password != null) {
- dest.writeString(password);
- }
- }
-
- public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() {
- public DownloadRequest createFromParcel(Parcel in) {
- return new DownloadRequest(in);
- }
-
- public DownloadRequest[] newArray(int size) {
- return new DownloadRequest[size];
- }
- };
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- DownloadRequest that = (DownloadRequest) o;
-
- if (deleteOnFailure != that.deleteOnFailure) return false;
- if (feedfileId != that.feedfileId) return false;
- if (feedfileType != that.feedfileType) return false;
- if (progressPercent != that.progressPercent) return false;
- if (size != that.size) return false;
- if (soFar != that.soFar) return false;
- if (statusMsg != that.statusMsg) return false;
- if (destination != null ? !destination.equals(that.destination) : that.destination != null)
- return false;
- if (password != null ? !password.equals(that.password) : that.password != null)
- return false;
- if (source != null ? !source.equals(that.source) : that.source != null) return false;
- if (title != null ? !title.equals(that.title) : that.title != null) return false;
- if (username != null ? !username.equals(that.username) : that.username != null)
- return false;
-
- return true;
- }
-
- @Override
- public int hashCode() {
- int result = destination != null ? destination.hashCode() : 0;
- result = 31 * result + (source != null ? source.hashCode() : 0);
- result = 31 * result + (title != null ? title.hashCode() : 0);
- result = 31 * result + (username != null ? username.hashCode() : 0);
- result = 31 * result + (password != null ? password.hashCode() : 0);
- result = 31 * result + (deleteOnFailure ? 1 : 0);
- result = 31 * result + (int) (feedfileId ^ (feedfileId >>> 32));
- result = 31 * result + feedfileType;
- result = 31 * result + progressPercent;
- result = 31 * result + (int) (soFar ^ (soFar >>> 32));
- result = 31 * result + (int) (size ^ (size >>> 32));
- result = 31 * result + statusMsg;
- return result;
- }
-
- public String getDestination() {
- return destination;
- }
-
- public String getSource() {
- return source;
- }
-
- public String getTitle() {
- return title;
- }
-
- public long getFeedfileId() {
- return feedfileId;
- }
-
- public int getFeedfileType() {
- return feedfileType;
- }
-
- public int getProgressPercent() {
- return progressPercent;
- }
-
- public void setProgressPercent(int progressPercent) {
- this.progressPercent = progressPercent;
- }
-
- public long getSoFar() {
- return soFar;
- }
-
- public void setSoFar(long soFar) {
- this.soFar = soFar;
- }
-
- public long getSize() {
- return size;
- }
-
- public void setSize(long size) {
- this.size = size;
- }
-
- public int getStatusMsg() {
- return statusMsg;
- }
-
- public void setStatusMsg(int statusMsg) {
- this.statusMsg = statusMsg;
- }
-
- public String getUsername() {
- return username;
- }
-
- public String getPassword() {
- return password;
- }
-
- public void setUsername(String username) {
- this.username = username;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-
- public boolean isDeleteOnFailure() {
- return deleteOnFailure;
- }
-}
diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java
deleted file mode 100644
index 63be91b57..000000000
--- a/src/de/danoeh/antennapod/service/download/DownloadService.java
+++ /dev/null
@@ -1,1230 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-import android.annotation.SuppressLint;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.MediaMetadataRetriever;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-import android.webkit.URLUtil;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-import org.apache.http.HttpStatus;
-import org.xml.sax.SAXException;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CompletionService;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorCompletionService;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.RejectedExecutionHandler;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import javax.xml.parsers.ParserConfigurationException;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.DownloadAuthenticationActivity;
-import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.adapter.NavListAdapter;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.feed.FeedPreferences;
-import de.danoeh.antennapod.fragment.DownloadsFragment;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.syndication.handler.FeedHandler;
-import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException;
-import de.danoeh.antennapod.util.ChapterUtils;
-import de.danoeh.antennapod.util.DownloadError;
-import de.danoeh.antennapod.util.InvalidFeedException;
-
-/**
- * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent.
- * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of
- * the intent.
- * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the
- * type of the feedfile.
- */
-public class DownloadService extends Service {
- private static final String TAG = "DownloadService";
-
- /**
- * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the
- * object whose download should be cancelled.
- */
- public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.service.cancelDownload";
-
- /**
- * Cancels all running downloads.
- */
- public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.service.cancelAllDownloads";
-
- /**
- * Extra for ACTION_CANCEL_DOWNLOAD
- */
- public static final String EXTRA_DOWNLOAD_URL = "downloadUrl";
-
- /**
- * Sent by the DownloadService when the content of the downloads list
- * changes.
- */
- public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.service.downloadsContentChanged";
-
- /**
- * Extra for ACTION_ENQUEUE_DOWNLOAD intent.
- */
- public static final String EXTRA_REQUEST = "request";
-
- /**
- * Stores new media files that will be queued for auto-download if possible.
- */
- private List<Long> newMediaFiles;
-
- /**
- * Contains all completed downloads that have not been included in the report yet.
- */
- private List<DownloadStatus> reportQueue;
-
- private ExecutorService syncExecutor;
- private CompletionService<Downloader> downloadExecutor;
- private FeedSyncThread feedSyncThread;
-
- /**
- * Number of threads of downloadExecutor.
- */
- private static final int NUM_PARALLEL_DOWNLOADS = 6;
-
- private DownloadRequester requester;
-
-
- private NotificationCompat.Builder notificationCompatBuilder;
- private Notification.BigTextStyle notificationBuilder;
- private int NOTIFICATION_ID = 2;
- private int REPORT_ID = 3;
-
- /**
- * Currently running downloads.
- */
- private List<Downloader> downloads;
-
- /**
- * Number of running downloads.
- */
- private AtomicInteger numberOfDownloads;
-
- /**
- * True if service is running.
- */
- public static boolean isRunning = false;
-
- private Handler handler;
-
- private NotificationUpdater notificationUpdater;
- private ScheduledFuture notificationUpdaterFuture;
- private static final int SCHED_EX_POOL_SIZE = 1;
- private ScheduledThreadPoolExecutor schedExecutor;
-
- private final IBinder mBinder = new LocalBinder();
-
- public class LocalBinder extends Binder {
- public DownloadService getService() {
- return DownloadService.this;
- }
- }
-
- private Thread downloadCompletionThread = new Thread() {
- private static final String TAG = "downloadCompletionThread";
-
- @Override
- public void run() {
- if (BuildConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started");
- while (!isInterrupted()) {
- try {
- Downloader downloader = downloadExecutor.take().get();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received 'Download Complete' - message.");
- removeDownload(downloader);
- DownloadStatus status = downloader.getResult();
- boolean successful = status.isSuccessful();
-
- final int type = status.getFeedfileType();
- if (successful) {
- if (type == Feed.FEEDFILETYPE_FEED) {
- handleCompletedFeedDownload(downloader
- .getDownloadRequest());
- } else if (type == FeedImage.FEEDFILETYPE_FEEDIMAGE) {
- handleCompletedImageDownload(status, downloader.getDownloadRequest());
- } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
- handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest());
- }
- } else {
- numberOfDownloads.decrementAndGet();
- if (!status.isCancelled()) {
- if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) {
- postAuthenticationNotification(downloader.getDownloadRequest());
- } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
- && Integer.valueOf(status.getReasonDetailed()) == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
-
- Log.d(TAG, "Requested invalid range, restarting download from the beginning");
- FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
- DownloadRequester.getInstance().download(DownloadService.this, downloader.getDownloadRequest());
- } else {
- Log.e(TAG, "Download failed");
- saveDownloadStatus(status);
- handleFailedDownload(status, downloader.getDownloadRequest());
- }
- }
- sendDownloadHandledIntent();
- queryDownloadsAsync();
- }
- } catch (InterruptedException e) {
- if (BuildConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted");
- } catch (ExecutionException e) {
- e.printStackTrace();
- numberOfDownloads.decrementAndGet();
- }
- }
- if (BuildConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread");
- }
- };
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent.getParcelableExtra(EXTRA_REQUEST) != null) {
- onDownloadQueued(intent);
- } else if (numberOfDownloads.get() == 0) {
- stopSelf();
- }
- return Service.START_NOT_STICKY;
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onCreate() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Service started");
- isRunning = true;
- handler = new Handler();
- newMediaFiles = Collections.synchronizedList(new ArrayList<Long>());
- reportQueue = Collections.synchronizedList(new ArrayList<DownloadStatus>());
- downloads = new ArrayList<Downloader>();
- numberOfDownloads = new AtomicInteger(0);
-
- IntentFilter cancelDownloadReceiverFilter = new IntentFilter();
- cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS);
- cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD);
- registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter);
- syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- });
- downloadExecutor = new ExecutorCompletionService<Downloader>(
- Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS,
- new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- }
- )
- );
- schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE,
- new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- }, new RejectedExecutionHandler() {
-
- @Override
- public void rejectedExecution(Runnable r,
- ThreadPoolExecutor executor) {
- Log.w(TAG, "SchedEx rejected submission of new task");
- }
- }
- );
- downloadCompletionThread.start();
- feedSyncThread = new FeedSyncThread();
- feedSyncThread.start();
-
- setupNotificationBuilders();
- requester = DownloadRequester.getInstance();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
-
- @Override
- public void onDestroy() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Service shutting down");
- isRunning = false;
- updateReport();
-
- stopForeground(true);
- NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- nm.cancel(NOTIFICATION_ID);
-
- downloadCompletionThread.interrupt();
- syncExecutor.shutdown();
- schedExecutor.shutdown();
- feedSyncThread.shutdown();
- cancelNotificationUpdater();
- unregisterReceiver(cancelDownloadReceiver);
-
- if (!newMediaFiles.isEmpty()) {
- DBTasks.autodownloadUndownloadedItems(getApplicationContext(),
- ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()])));
- }
- }
-
- @SuppressLint("NewApi")
- private void setupNotificationBuilders() {
- Intent intent = new Intent(this, 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);
-
- PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT
- );
-
-
- Bitmap icon = BitmapFactory.decodeResource(getResources(),
- R.drawable.stat_notify_sync);
-
- if (android.os.Build.VERSION.SDK_INT >= 16) {
- notificationBuilder = new Notification.BigTextStyle(
- new Notification.Builder(this).setOngoing(true)
- .setContentIntent(pIntent).setLargeIcon(icon)
- .setSmallIcon(R.drawable.stat_notify_sync)
- );
- } else {
- notificationCompatBuilder = new NotificationCompat.Builder(this)
- .setOngoing(true).setContentIntent(pIntent)
- .setLargeIcon(icon)
- .setSmallIcon(R.drawable.stat_notify_sync);
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Notification set up");
- }
-
- /**
- * Updates the contents of the service's notifications. Should be called
- * before setupNotificationBuilders.
- */
- @SuppressLint("NewApi")
- private Notification updateNotifications() {
- String contentTitle = getString(R.string.download_notification_title);
- int numDownloads = requester.getNumberOfDownloads();
- String downloadsLeft;
- if (numDownloads > 0) {
- downloadsLeft = requester.getNumberOfDownloads()
- + getString(R.string.downloads_left);
- } else {
- downloadsLeft = getString(R.string.downloads_processing);
- }
- if (android.os.Build.VERSION.SDK_INT >= 16) {
-
- if (notificationBuilder != null) {
-
- StringBuilder bigText = new StringBuilder("");
- for (int i = 0; i < downloads.size(); i++) {
- Downloader downloader = downloads.get(i);
- final DownloadRequest request = downloader
- .getDownloadRequest();
- if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
- if (request.getTitle() != null) {
- if (i > 0) {
- bigText.append("\n");
- }
- bigText.append("\u2022 " + request.getTitle());
- }
- } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
- if (request.getTitle() != null) {
- if (i > 0) {
- bigText.append("\n");
- }
- bigText.append("\u2022 " + request.getTitle()
- + " (" + request.getProgressPercent()
- + "%)");
- }
- }
-
- }
- notificationBuilder.setSummaryText(downloadsLeft);
- notificationBuilder.setBigContentTitle(contentTitle);
- if (bigText != null) {
- notificationBuilder.bigText(bigText.toString());
- }
- return notificationBuilder.build();
- }
- } else {
- if (notificationCompatBuilder != null) {
- notificationCompatBuilder.setContentTitle(contentTitle);
- notificationCompatBuilder.setContentText(downloadsLeft);
- return notificationCompatBuilder.build();
- }
- }
- return null;
- }
-
- private Downloader getDownloader(String downloadUrl) {
- for (Downloader downloader : downloads) {
- if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) {
- return downloader;
- }
- }
- return null;
- }
-
- private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) {
- String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL);
- Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra");
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Cancelling download with url " + url);
- Downloader d = getDownloader(url);
- if (d != null) {
- d.cancel();
- } else {
- Log.e(TAG, "Could not cancel download with url " + url);
- }
-
- } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) {
- for (Downloader d : downloads) {
- d.cancel();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Cancelled all downloads");
- }
- sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
-
- }
- queryDownloads();
- }
-
- };
-
- private void onDownloadQueued(Intent intent) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received enqueue request");
- DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST);
- if (request == null) {
- throw new IllegalArgumentException(
- "ACTION_ENQUEUE_DOWNLOAD intent needs request extra");
- }
-
- Downloader downloader = getDownloader(request);
- if (downloader != null) {
- numberOfDownloads.incrementAndGet();
- downloads.add(downloader);
- downloadExecutor.submit(downloader);
- sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
- }
-
- queryDownloads();
- }
-
- private Downloader getDownloader(DownloadRequest request) {
- if (URLUtil.isHttpUrl(request.getSource())
- || URLUtil.isHttpsUrl(request.getSource())) {
- return new HttpDownloader(request);
- }
- Log.e(TAG,
- "Could not find appropriate downloader for "
- + request.getSource()
- );
- return null;
- }
-
- /**
- * Remove download from the DownloadRequester list and from the
- * DownloadService list.
- */
- private void removeDownload(final Downloader d) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Removing downloader: "
- + d.getDownloadRequest().getSource());
- boolean rc = downloads.remove(d);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Result of downloads.remove: " + rc);
- DownloadRequester.getInstance().removeDownload(d.getDownloadRequest());
- sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED));
- }
- });
- }
-
- /**
- * Adds a new DownloadStatus object to the list of completed downloads and
- * saves it in the database
- *
- * @param status the download that is going to be saved
- */
- private void saveDownloadStatus(DownloadStatus status) {
- reportQueue.add(status);
- DBWriter.addDownloadStatus(this, status);
- }
-
- private void sendDownloadHandledIntent() {
- EventDistributor.getInstance().sendDownloadHandledBroadcast();
- }
-
- /**
- * Creates a notification at the end of the service lifecycle to notify the
- * user about the number of completed downloads. A report will only be
- * created if the number of successfully downloaded feeds is bigger than 1
- * or if there is at least one failed download which is not an image or if
- * there is at least one downloaded media file.
- */
- private void updateReport() {
- // check if report should be created
- boolean createReport = false;
- int successfulDownloads = 0;
- int failedDownloads = 0;
-
- // a download report is created if at least one download has failed
- // (excluding failed image downloads)
- for (DownloadStatus status : reportQueue) {
- if (status.isSuccessful()) {
- successfulDownloads++;
- } else if (!status.isCancelled()) {
- if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) {
- createReport = true;
- }
- failedDownloads++;
- }
- }
-
- if (createReport) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Creating report");
- Intent intent = new Intent(this, 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);
-
- // create notification object
- Notification notification = new NotificationCompat.Builder(this)
- .setTicker(
- getString(de.danoeh.antennapod.R.string.download_report_title))
- .setContentTitle(
- getString(de.danoeh.antennapod.R.string.download_report_title))
- .setContentText(
- String.format(
- getString(R.string.download_report_content),
- successfulDownloads, failedDownloads)
- )
- .setSmallIcon(R.drawable.stat_notify_sync)
- .setLargeIcon(
- BitmapFactory.decodeResource(getResources(),
- R.drawable.stat_notify_sync)
- )
- .setContentIntent(
- PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
- )
- .setAutoCancel(true).build();
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(REPORT_ID, notification);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "No report is created");
- }
- reportQueue.clear();
- }
-
- /**
- * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is
- * used from a thread other than the main thread.
- */
- void queryDownloadsAsync() {
- handler.post(new Runnable() {
- public void run() {
- queryDownloads();
- ;
- }
- });
- }
-
- /**
- * Check if there's something else to download, otherwise stop
- */
- void queryDownloads() {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, numberOfDownloads.get() + " downloads left");
- }
-
- if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown");
- stopSelf();
- } else {
- setupNotificationUpdater();
- startForeground(NOTIFICATION_ID, updateNotifications());
- }
- }
-
- private void postAuthenticationNotification(final DownloadRequest downloadRequest) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- final String resourceTitle = (downloadRequest.getTitle() != null)
- ? downloadRequest.getTitle() : downloadRequest.getSource();
-
- final Intent activityIntent = new Intent(getApplicationContext(), DownloadAuthenticationActivity.class);
- activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, downloadRequest);
- activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true);
- final PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, activityIntent, PendingIntent.FLAG_ONE_SHOT);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this);
- builder.setTicker(getText(R.string.authentication_notification_title))
- .setContentTitle(getText(R.string.authentication_notification_title))
- .setContentText(getText(R.string.authentication_notification_msg))
- .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg)
- + ": " + resourceTitle))
- .setSmallIcon(R.drawable.ic_stat_authentication)
- .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication))
- .setAutoCancel(true)
- .setContentIntent(contentIntent);
- Notification n = builder.build();
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(downloadRequest.getSource().hashCode(), n);
- }
- });
- }
-
- /**
- * Is called whenever a Feed is downloaded
- */
- private void handleCompletedFeedDownload(DownloadRequest request) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Handling completed Feed Download");
- feedSyncThread.submitCompletedDownload(request);
-
- }
-
- /**
- * Is called whenever a Feed-Image is downloaded
- */
- private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Handling completed Image Download");
- syncExecutor.execute(new ImageHandlerThread(status, request));
- }
-
- /**
- * Is called whenever a FeedMedia is downloaded.
- */
- private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Handling completed FeedMedia Download");
- syncExecutor.execute(new MediaHandlerThread(status, request));
- }
-
- private void handleFailedDownload(DownloadStatus status, DownloadRequest request) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download");
- syncExecutor.execute(new FailedDownloadHandler(status, request));
- }
-
- /**
- * Takes a single Feed, parses the corresponding file and refreshes
- * information in the manager
- */
- class FeedSyncThread extends Thread {
- private static final String TAG = "FeedSyncThread";
-
- private BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<DownloadRequest>();
- private CompletionService<Feed> parserService = new ExecutorCompletionService<Feed>(Executors.newSingleThreadExecutor());
- private ExecutorService dbService = Executors.newSingleThreadExecutor();
- private Future<?> dbUpdateFuture;
- private volatile boolean isActive = true;
- private volatile boolean isCollectingRequests = false;
-
- private final long WAIT_TIMEOUT = 3000;
-
-
- /**
- * Waits for completed requests. Once the first request has been taken, the method will wait WAIT_TIMEOUT ms longer to
- * collect more completed requests.
- *
- * @return Collected feeds or null if the method has been interrupted during the first waiting period.
- */
- private List<Feed> collectCompletedRequests() {
- List<Feed> results = new LinkedList<Feed>();
- DownloadRequester requester = DownloadRequester.getInstance();
- int tasks = 0;
-
- try {
- DownloadRequest request = completedRequests.take();
- parserService.submit(new FeedParserTask(request));
- tasks++;
- } catch (InterruptedException e) {
- return null;
- }
-
- tasks += pollCompletedDownloads();
-
- isCollectingRequests = true;
-
- if (requester.isDownloadingFeeds()) {
- // wait for completion of more downloads
- long startTime = System.currentTimeMillis();
- long currentTime = startTime;
- while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) {
- try {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms");
- sleep(startTime + WAIT_TIMEOUT - currentTime);
- } catch (InterruptedException e) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "interrupted while waiting for more downloads");
- tasks += pollCompletedDownloads();
- } finally {
- currentTime = System.currentTimeMillis();
- }
- }
-
- tasks += pollCompletedDownloads();
-
- }
-
- isCollectingRequests = false;
-
- for (int i = 0; i < tasks; i++) {
- try {
- Feed f = parserService.take().get();
- if (f != null) {
- results.add(f);
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
-
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
-
- return results;
- }
-
- private int pollCompletedDownloads() {
- int tasks = 0;
- for (int i = 0; i < completedRequests.size(); i++) {
- parserService.submit(new FeedParserTask(completedRequests.poll()));
- tasks++;
- }
- return tasks;
- }
-
- @Override
- public void run() {
- while (isActive) {
- final List<Feed> feeds = collectCompletedRequests();
-
- if (feeds == null) {
- continue;
- }
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + feeds.size() + " feeds");
-
- for (Feed feed : feeds) {
- removeDuplicateImages(feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet.
- }
-
- // Save information of feed in DB
- if (dbUpdateFuture != null) {
- try {
- dbUpdateFuture.get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
-
- dbUpdateFuture = dbService.submit(new Runnable() {
- @Override
- public void run() {
- Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, feeds.toArray(new Feed[feeds.size()]));
-
- for (Feed savedFeed : savedFeeds) {
- // Download Feed Image if provided and not downloaded
- if (savedFeed.getImage() != null
- && savedFeed.getImage().isDownloaded() == false) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Feed has image; Downloading....");
- savedFeed.getImage().setOwner(savedFeed);
- final Feed savedFeedRef = savedFeed;
- try {
- requester.downloadImage(DownloadService.this,
- savedFeedRef.getImage());
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- DBWriter.addDownloadStatus(
- DownloadService.this,
- new DownloadStatus(
- savedFeedRef.getImage(),
- savedFeedRef
- .getImage()
- .getHumanReadableIdentifier(),
- DownloadError.ERROR_REQUEST_ERROR,
- false, e.getMessage()
- )
- );
- }
- }
-
- // queue new media files for automatic download
- for (FeedItem item : savedFeed.getItems()) {
- if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) {
- newMediaFiles.add(item.getMedia().getId());
- }
- }
-
- numberOfDownloads.decrementAndGet();
- }
-
- sendDownloadHandledIntent();
-
- queryDownloadsAsync();
- }
- });
-
- }
-
- if (dbUpdateFuture != null) {
- try {
- dbUpdateFuture.get();
- } catch (InterruptedException e) {
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down");
-
- }
-
- private class FeedParserTask implements Callable<Feed> {
-
- private DownloadRequest request;
-
- private FeedParserTask(DownloadRequest request) {
- this.request = request;
- }
-
- @Override
- public Feed call() throws Exception {
- return parseFeed(request);
- }
- }
-
- private Feed parseFeed(DownloadRequest request) {
- Feed savedFeed = null;
-
- Feed feed = new Feed(request.getSource(), new Date());
- feed.setFile_url(request.getDestination());
- feed.setId(request.getFeedfileId());
- feed.setDownloaded(true);
- feed.setPreferences(new FeedPreferences(0, true, request.getUsername(), request.getPassword()));
-
- DownloadError reason = null;
- String reasonDetailed = null;
- boolean successful = true;
- FeedHandler feedHandler = new FeedHandler();
-
- try {
- feed = feedHandler.parseFeed(feed).feed;
- if (BuildConfig.DEBUG)
- Log.d(TAG, feed.getTitle() + " parsed");
- if (checkFeedData(feed) == false) {
- throw new InvalidFeedException();
- }
-
- } catch (SAXException e) {
- successful = false;
- e.printStackTrace();
- reason = DownloadError.ERROR_PARSER_EXCEPTION;
- reasonDetailed = e.getMessage();
- } catch (IOException e) {
- successful = false;
- e.printStackTrace();
- reason = DownloadError.ERROR_PARSER_EXCEPTION;
- reasonDetailed = e.getMessage();
- } catch (ParserConfigurationException e) {
- successful = false;
- e.printStackTrace();
- reason = DownloadError.ERROR_PARSER_EXCEPTION;
- reasonDetailed = e.getMessage();
- } catch (UnsupportedFeedtypeException e) {
- e.printStackTrace();
- successful = false;
- reason = DownloadError.ERROR_UNSUPPORTED_TYPE;
- reasonDetailed = e.getMessage();
- } catch (InvalidFeedException e) {
- e.printStackTrace();
- successful = false;
- reason = DownloadError.ERROR_PARSER_EXCEPTION;
- reasonDetailed = e.getMessage();
- }
-
- // cleanup();
- if (savedFeed == null) {
- savedFeed = feed;
- }
-
-
- if (successful) {
- return savedFeed;
- } else {
- numberOfDownloads.decrementAndGet();
- saveDownloadStatus(new DownloadStatus(savedFeed,
- savedFeed.getHumanReadableIdentifier(), reason, successful,
- reasonDetailed));
- return null;
- }
- }
-
-
- /**
- * Checks if the feed was parsed correctly.
- */
- private boolean checkFeedData(Feed feed) {
- if (feed.getTitle() == null) {
- Log.e(TAG, "Feed has no title.");
- return false;
- }
- if (!hasValidFeedItems(feed)) {
- Log.e(TAG, "Feed has invalid items");
- return false;
- }
- return true;
- }
-
- /**
- * Checks if the FeedItems of this feed have images that point
- * to the same URL. If two FeedItems have an image that points to
- * the same URL, the reference of the second item is removed, so that every image
- * reference is unique.
- */
- private void removeDuplicateImages(Feed feed) {
- for (int x = 0; x < feed.getItems().size(); x++) {
- for (int y = x + 1; y < feed.getItems().size(); y++) {
- FeedItem item1 = feed.getItems().get(x);
- FeedItem item2 = feed.getItems().get(y);
- if (item1.hasItemImage() && item2.hasItemImage()) {
- if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) {
- item2.setImage(null);
- }
- }
- }
- }
- }
-
- private boolean hasValidFeedItems(Feed feed) {
- for (FeedItem item : feed.getItems()) {
- if (item.getTitle() == null) {
- Log.e(TAG, "Item has no title");
- return false;
- }
- if (item.getPubDate() == null) {
- Log.e(TAG,
- "Item has no pubDate. Using current time as pubDate");
- if (item.getTitle() != null) {
- Log.e(TAG, "Title of invalid item: " + item.getTitle());
- }
- item.setPubDate(new Date());
- }
- }
- return true;
- }
-
- /**
- * Delete files that aren't needed anymore
- */
- private void cleanup(Feed feed) {
- if (feed.getFile_url() != null) {
- if (new File(feed.getFile_url()).delete())
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Successfully deleted cache file.");
- else
- Log.e(TAG, "Failed to delete cache file.");
- feed.setFile_url(null);
- } else if (BuildConfig.DEBUG) {
- Log.d(TAG, "Didn't delete cache file: File url is not set.");
- }
- }
-
- public void shutdown() {
- isActive = false;
- if (isCollectingRequests) {
- interrupt();
- }
- }
-
- public void submitCompletedDownload(DownloadRequest request) {
- completedRequests.offer(request);
- if (isCollectingRequests) {
- interrupt();
- }
- }
-
- }
-
- /**
- * Handles failed downloads.
- * <p/>
- * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location
- * of the downloaded file.
- * <p/>
- * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails.
- */
- class FailedDownloadHandler implements Runnable {
-
- private DownloadRequest request;
- private DownloadStatus status;
-
- FailedDownloadHandler(DownloadStatus status, DownloadRequest request) {
- this.request = request;
- this.status = status;
- }
-
- @Override
- public void run() {
- if (request.isDeleteOnFailure()) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true");
- } else {
- File dest = new File(request.getDestination());
- if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
- Log.d(TAG, "File has been partially downloaded. Writing file url");
- FeedMedia media = DBReader.getFeedMedia(DownloadService.this, request.getFeedfileId());
- media.setFile_url(request.getDestination());
- try {
- DBWriter.setFeedMedia(DownloadService.this, media).get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
-
- /**
- * Handles a completed image download.
- */
- class ImageHandlerThread implements Runnable {
-
- private DownloadRequest request;
- private DownloadStatus status;
-
- public ImageHandlerThread(DownloadStatus status, DownloadRequest request) {
- Validate.notNull(status);
- Validate.notNull(request);
-
- this.status = status;
- this.request = request;
- }
-
- @Override
- public void run() {
- FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId());
- if (image == null) {
- throw new IllegalStateException("Could not find downloaded image in database");
- }
-
- image.setFile_url(request.getDestination());
- image.setDownloaded(true);
-
- saveDownloadStatus(status);
- sendDownloadHandledIntent();
- DBWriter.setFeedImage(DownloadService.this, image);
- numberOfDownloads.decrementAndGet();
- queryDownloadsAsync();
- }
- }
-
- /**
- * Handles a completed media download.
- */
- class MediaHandlerThread implements Runnable {
-
- private DownloadRequest request;
- private DownloadStatus status;
-
- public MediaHandlerThread(DownloadStatus status, DownloadRequest request) {
- Validate.notNull(status);
- Validate.notNull(request);
-
- this.status = status;
- this.request = request;
- }
-
- @Override
- public void run() {
- FeedMedia media = DBReader.getFeedMedia(DownloadService.this,
- request.getFeedfileId());
- if (media == null) {
- throw new IllegalStateException(
- "Could not find downloaded media object in database");
- }
- boolean chaptersRead = false;
- media.setDownloaded(true);
- media.setFile_url(request.getDestination());
-
- // Get duration
- MediaMetadataRetriever mmr = null;
- try {
- mmr = new MediaMetadataRetriever();
- mmr.setDataSource(media.getFile_url());
- String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
- media.setDuration(Integer.parseInt(durationStr));
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Duration of file is " + media.getDuration());
- } catch (NumberFormatException e) {
- e.printStackTrace();
- } catch (RuntimeException e) {
- e.printStackTrace();
- } finally {
- if (mmr != null) {
- mmr.release();
- }
- }
-
- if (media.getItem().getChapters() == null) {
- ChapterUtils.loadChaptersFromFileUrl(media);
- if (media.getItem().getChapters() != null) {
- chaptersRead = true;
- }
- }
-
- try {
- if (chaptersRead) {
- DBWriter.setFeedItem(DownloadService.this, media.getItem()).get();
- }
- DBWriter.setFeedMedia(DownloadService.this, media).get();
- if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) {
- DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get();
- }
- } catch (ExecutionException e) {
- e.printStackTrace();
- status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage());
- } catch (InterruptedException e) {
- e.printStackTrace();
- status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage());
- }
-
- saveDownloadStatus(status);
- sendDownloadHandledIntent();
-
- numberOfDownloads.decrementAndGet();
- queryDownloadsAsync();
- }
- }
-
- /**
- * Schedules the notification updater task if it hasn't been scheduled yet.
- */
- private void setupNotificationUpdater() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Setting up notification updater");
- if (notificationUpdater == null) {
- notificationUpdater = new NotificationUpdater();
- notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate(
- notificationUpdater, 5L, 5L, TimeUnit.SECONDS);
- }
- }
-
- private void cancelNotificationUpdater() {
- boolean result = false;
- if (notificationUpdaterFuture != null) {
- result = notificationUpdaterFuture.cancel(true);
- }
- notificationUpdater = null;
- notificationUpdaterFuture = null;
- Log.d(TAG, "NotificationUpdater cancelled. Result: " + result);
- }
-
- private class NotificationUpdater implements Runnable {
- public void run() {
- handler.post(new Runnable() {
- @Override
- public void run() {
- Notification n = updateNotifications();
- if (n != null) {
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(NOTIFICATION_ID, n);
- }
- }
- });
- }
- }
-
- public List<Downloader> getDownloads() {
- return downloads;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/service/download/DownloadStatus.java b/src/de/danoeh/antennapod/service/download/DownloadStatus.java
deleted file mode 100644
index 1d76770bb..000000000
--- a/src/de/danoeh/antennapod/service/download/DownloadStatus.java
+++ /dev/null
@@ -1,181 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.feed.FeedFile;
-import de.danoeh.antennapod.util.DownloadError;
-
-import java.util.Date;
-
-/** Contains status attributes for one download */
-public class DownloadStatus {
- /**
- * Downloaders should use this constant for the size attribute if necessary
- * so that the listadapters etc. can react properly.
- */
- public static final int SIZE_UNKNOWN = -1;
-
- // ----------------------------------- ATTRIBUTES STORED IN DB
- /** Unique id for storing the object in database. */
- protected long id;
- /**
- * A human-readable string which is shown to the user so that he can
- * identify the download. Should be the title of the item/feed/media or the
- * URL if the download has no other title.
- */
- protected String title;
- protected DownloadError reason;
- /**
- * A message which can be presented to the user to give more information.
- * Should be null if Download was successful.
- */
- protected String reasonDetailed;
- protected boolean successful;
- protected Date completionDate;
- protected long feedfileId;
- /**
- * Is used to determine the type of the feedfile even if the feedfile does
- * not exist anymore. The value should be FEEDFILETYPE_FEED,
- * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA
- */
- protected int feedfileType;
-
- // ------------------------------------ NOT STORED IN DB
- protected boolean done;
- protected boolean cancelled;
-
- /** Constructor for restoring Download status entries from DB. */
- public DownloadStatus(long id, String title, long feedfileId,
- int feedfileType, boolean successful, DownloadError reason,
- Date completionDate, String reasonDetailed) {
- this.id = id;
- this.title = title;
- this.done = true;
- this.feedfileId = feedfileId;
- this.reason = reason;
- this.successful = successful;
- this.completionDate = (Date) completionDate.clone();
- this.reasonDetailed = reasonDetailed;
- this.feedfileType = feedfileType;
- }
-
- public DownloadStatus(DownloadRequest request, DownloadError reason,
- boolean successful, boolean cancelled, String reasonDetailed) {
- Validate.notNull(request);
-
- this.title = request.getTitle();
- this.feedfileId = request.getFeedfileId();
- this.feedfileType = request.getFeedfileType();
- this.reason = reason;
- this.successful = successful;
- this.cancelled = cancelled;
- this.reasonDetailed = reasonDetailed;
- this.completionDate = new Date();
- }
-
- /** Constructor for creating new completed downloads. */
- public DownloadStatus(FeedFile feedfile, String title, DownloadError reason,
- boolean successful, String reasonDetailed) {
- Validate.notNull(feedfile);
-
- this.title = title;
- this.done = true;
- this.feedfileId = feedfile.getId();
- this.feedfileType = feedfile.getTypeAsInt();
- this.reason = reason;
- this.successful = successful;
- this.completionDate = new Date();
- this.reasonDetailed = reasonDetailed;
- }
-
- /** Constructor for creating new completed downloads. */
- public DownloadStatus(long feedfileId, int feedfileType, String title,
- DownloadError reason, boolean successful, String reasonDetailed) {
- this.title = title;
- this.done = true;
- this.feedfileId = feedfileId;
- this.feedfileType = feedfileType;
- this.reason = reason;
- this.successful = successful;
- this.completionDate = new Date();
- this.reasonDetailed = reasonDetailed;
- }
-
- @Override
- public String toString() {
- return "DownloadStatus [id=" + id + ", title=" + title + ", reason="
- + reason + ", reasonDetailed=" + reasonDetailed
- + ", successful=" + successful + ", completionDate="
- + completionDate + ", feedfileId=" + feedfileId
- + ", feedfileType=" + feedfileType + ", done=" + done
- + ", cancelled=" + cancelled + "]";
- }
-
- public long getId() {
- return id;
- }
-
- public String getTitle() {
- return title;
- }
-
- public DownloadError getReason() {
- return reason;
- }
-
- public String getReasonDetailed() {
- return reasonDetailed;
- }
-
- public boolean isSuccessful() {
- return successful;
- }
-
- public Date getCompletionDate() {
- return (Date) completionDate.clone();
- }
-
- public long getFeedfileId() {
- return feedfileId;
- }
-
- public int getFeedfileType() {
- return feedfileType;
- }
-
- public boolean isDone() {
- return done;
- }
-
- public boolean isCancelled() {
- return cancelled;
- }
-
- public void setSuccessful() {
- this.successful = true;
- this.reason = DownloadError.SUCCESS;
- this.done = true;
- }
-
- public void setFailed(DownloadError reason, String reasonDetailed) {
- this.successful = false;
- this.reason = reason;
- this.reasonDetailed = reasonDetailed;
- this.done = true;
- }
-
- public void setCancelled() {
- this.successful = false;
- this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED;
- this.done = true;
- this.cancelled = true;
- }
-
- public void setCompletionDate(Date completionDate) {
- this.completionDate = (Date) completionDate.clone();
- }
-
- public void setId(long id) {
- this.id = id;
- }
-} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/service/download/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java
deleted file mode 100644
index 80cc5b3f8..000000000
--- a/src/de/danoeh/antennapod/service/download/Downloader.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-import android.content.Context;
-import android.net.wifi.WifiManager;
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.R;
-
-import java.util.concurrent.Callable;
-
-/** Downloads files */
-public abstract class Downloader implements Callable<Downloader> {
- private static final String TAG = "Downloader";
-
- protected volatile boolean finished;
-
- protected volatile boolean cancelled;
-
- protected DownloadRequest request;
- protected DownloadStatus result;
-
- public Downloader(DownloadRequest request) {
- super();
- this.request = request;
- this.request.setStatusMsg(R.string.download_pending);
- this.cancelled = false;
- this.result = new DownloadStatus(request, null, false, false, null);
- }
-
- protected abstract void download();
-
- public final Downloader call() {
- WifiManager wifiManager = (WifiManager) PodcastApp.getInstance().getSystemService(Context.WIFI_SERVICE);
- WifiManager.WifiLock wifiLock = null;
- if (wifiManager != null) {
- wifiLock = wifiManager.createWifiLock(TAG);
- wifiLock.acquire();
- }
-
- download();
-
- if (wifiLock != null) {
- wifiLock.release();
- }
-
- if (result == null) {
- throw new IllegalStateException(
- "Downloader hasn't created DownloadStatus object");
- }
- finished = true;
- return this;
- }
-
- public DownloadRequest getDownloadRequest() {
- return request;
- }
-
- public DownloadStatus getResult() {
- return result;
- }
-
- public boolean isFinished() {
- return finished;
- }
-
- public void cancel() {
- cancelled = true;
- }
-
-} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/service/download/DownloaderCallback.java b/src/de/danoeh/antennapod/service/download/DownloaderCallback.java
deleted file mode 100644
index 08420e83a..000000000
--- a/src/de/danoeh/antennapod/service/download/DownloaderCallback.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-/**
- * Callback used by the Downloader-classes to notify the requester that the
- * download has completed.
- */
-public interface DownloaderCallback {
-
- public void onDownloadCompleted(Downloader downloader);
-}
diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java
deleted file mode 100644
index 7ae96dc07..000000000
--- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java
+++ /dev/null
@@ -1,246 +0,0 @@
-package de.danoeh.antennapod.service.download;
-
-import android.net.http.AndroidHttpClient;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.util.DownloadError;
-import de.danoeh.antennapod.util.StorageUtils;
-import de.danoeh.antennapod.util.URIUtil;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.auth.BasicScheme;
-import org.apache.http.message.BasicHeader;
-
-import java.io.*;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.net.UnknownHostException;
-
-public class HttpDownloader extends Downloader {
- private static final String TAG = "HttpDownloader";
-
- private static final int BUFFER_SIZE = 8 * 1024;
-
- public HttpDownloader(DownloadRequest request) {
- super(request);
- }
-
- @Override
- protected void download() {
- File destination = new File(request.getDestination());
- final boolean fileExists = destination.exists();
-
- if (request.isDeleteOnFailure() && fileExists) {
- Log.w(TAG, "File already exists");
- if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) {
- onFail(DownloadError.ERROR_FILE_EXISTS, null);
- return;
- } else {
- onSuccess();
- return;
- }
- }
-
- HttpClient httpClient = AntennapodHttpClient.getHttpClient();
- RandomAccessFile out = null;
- InputStream connection = null;
- try {
- HttpGet httpGet = new HttpGet(URIUtil.getURIFromRequestUrl(request.getSource()));
-
- // add authentication information
- String userInfo = httpGet.getURI().getUserInfo();
- if (userInfo != null) {
- String[] parts = userInfo.split(":");
- if (parts.length == 2) {
- httpGet.addHeader(BasicScheme.authenticate(
- new UsernamePasswordCredentials(parts[0], parts[1]),
- "UTF-8", false));
- }
- } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) {
- httpGet.addHeader(BasicScheme.authenticate(new UsernamePasswordCredentials(request.getUsername(),
- request.getPassword()), "UTF-8", false));
- }
-
- // add range header if necessary
- if (fileExists) {
- request.setSoFar(destination.length());
- httpGet.addHeader(new BasicHeader("Range",
- "bytes=" + request.getSoFar() + "-"));
- if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar());
- }
-
- HttpResponse response = httpClient.execute(httpGet);
- HttpEntity httpEntity = response.getEntity();
- int responseCode = response.getStatusLine().getStatusCode();
- Header contentEncodingHeader = response.getFirstHeader("Content-Encoding");
-
- final boolean isGzip = contentEncodingHeader != null &&
- contentEncodingHeader.getValue().equalsIgnoreCase("gzip");
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Response code is " + responseCode);
-
- if (responseCode / 100 != 2 || httpEntity == null) {
- final DownloadError error;
- final String details;
- if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
- error = DownloadError.ERROR_UNAUTHORIZED;
- details = String.valueOf(responseCode);
- } else {
- error = DownloadError.ERROR_HTTP_DATA_ERROR;
- details = String.valueOf(responseCode);
- }
- onFail(error, details);
- return;
- }
-
- if (!StorageUtils.storageAvailable(PodcastApp.getInstance())) {
- onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null);
- return;
- }
-
- connection = new BufferedInputStream(AndroidHttpClient
- .getUngzippedContent(httpEntity));
-
- Header[] contentRangeHeaders = (fileExists) ? response.getHeaders("Content-Range") : null;
-
- if (fileExists && responseCode == HttpStatus.SC_PARTIAL_CONTENT
- && contentRangeHeaders != null && contentRangeHeaders.length > 0) {
- String start = contentRangeHeaders[0].getValue().substring("bytes ".length(),
- contentRangeHeaders[0].getValue().indexOf("-"));
- request.setSoFar(Long.valueOf(start));
- Log.d(TAG, "Starting download at position " + request.getSoFar());
-
- out = new RandomAccessFile(destination, "rw");
- out.seek(request.getSoFar());
- } else {
- destination.delete();
- destination.createNewFile();
- out = new RandomAccessFile(destination, "rw");
- }
-
-
- byte[] buffer = new byte[BUFFER_SIZE];
- int count = 0;
- request.setStatusMsg(R.string.download_running);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Getting size of download");
- request.setSize(httpEntity.getContentLength() + request.getSoFar());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Size is " + request.getSize());
- if (request.getSize() < 0) {
- request.setSize(DownloadStatus.SIZE_UNKNOWN);
- }
-
- long freeSpace = StorageUtils.getFreeSpaceAvailable();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Free space is " + freeSpace);
-
- if (request.getSize() != DownloadStatus.SIZE_UNKNOWN
- && request.getSize() > freeSpace) {
- onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null);
- return;
- }
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Starting download");
- while (!cancelled
- && (count = connection.read(buffer)) != -1) {
- out.write(buffer, 0, count);
- request.setSoFar(request.getSoFar() + count);
- request.setProgressPercent((int) (((double) request
- .getSoFar() / (double) request
- .getSize()) * 100));
- }
- if (cancelled) {
- onCancelled();
- } else {
- // check if size specified in the response header is the same as the size of the
- // written file. This check cannot be made if compression was used
- if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN &&
- request.getSoFar() != request.getSize()) {
- onFail(DownloadError.ERROR_IO_ERROR,
- "Download completed but size: " +
- request.getSoFar() +
- " does not equal expected size " +
- request.getSize()
- );
- return;
- }
- onSuccess();
- }
-
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage());
- } catch (SocketTimeoutException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage());
- } catch (UnknownHostException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage());
- } catch (IOException e) {
- e.printStackTrace();
- onFail(DownloadError.ERROR_IO_ERROR, e.getMessage());
- } catch (NullPointerException e) {
- // might be thrown by connection.getInputStream()
- e.printStackTrace();
- onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource());
- } finally {
- IOUtils.closeQuietly(out);
- AntennapodHttpClient.cleanup();
- }
- }
-
- private void onSuccess() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Download was successful");
- result.setSuccessful();
- }
-
- private void onFail(DownloadError reason, String reasonDetailed) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "Download failed");
- }
- result.setFailed(reason, reasonDetailed);
- if (request.isDeleteOnFailure()) {
- cleanup();
- }
- }
-
- private void onCancelled() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Download was cancelled");
- result.setCancelled();
- cleanup();
- }
-
- /**
- * Deletes unfinished downloads.
- */
- private void cleanup() {
- if (request.getDestination() != null) {
- File dest = new File(request.getDestination());
- if (dest.exists()) {
- boolean rc = dest.delete();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Deleted file " + dest.getName() + "; Result: "
- + rc);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "cleanup() didn't delete file: does not exist.");
- }
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java
deleted file mode 100644
index 6c292c08a..000000000
--- a/src/de/danoeh/antennapod/service/playback/PlaybackService.java
+++ /dev/null
@@ -1,1132 +0,0 @@
-package de.danoeh.antennapod.service.playback;
-
-import android.annotation.SuppressLint;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.AudioManager;
-import android.media.MediaMetadataRetriever;
-import android.media.MediaPlayer;
-import android.media.RemoteControlClient;
-import android.media.RemoteControlClient.MetadataEditor;
-import android.os.AsyncTask;
-import android.os.Binder;
-import android.os.Build;
-import android.os.IBinder;
-import android.preference.PreferenceManager;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-import android.util.Pair;
-import android.view.KeyEvent;
-import android.view.SurfaceHolder;
-import android.widget.Toast;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.IOException;
-import java.util.List;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.AudioplayerActivity;
-import de.danoeh.antennapod.activity.VideoplayerActivity;
-import de.danoeh.antennapod.asynctask.PicassoProvider;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.receiver.MediaButtonReceiver;
-import de.danoeh.antennapod.receiver.PlayerWidget;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
-import de.danoeh.antennapod.util.playback.Playable;
-
-/**
- * Controls the MediaPlayer that plays a FeedMedia-file
- */
-public class PlaybackService extends Service {
- /**
- * Logging tag
- */
- private static final String TAG = "PlaybackService";
-
- /**
- * Parcelable of type Playable.
- */
- public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
- /**
- * True if media should be streamed.
- */
- public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
- /**
- * True if playback should be started immediately after media has been
- * prepared.
- */
- public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";
-
- public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";
-
- public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
- private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
- private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged";
-
- public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
- public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
- public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";
-
- /**
- * If the PlaybackService receives this action, it will stop playback and
- * try to shutdown.
- */
- public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";
-
- /**
- * If the PlaybackService receives this action, it will end playback of the
- * current episode and load the next episode if there is one available.
- */
- public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";
-
- /**
- * Used in NOTIFICATION_TYPE_RELOAD.
- */
- public static final int EXTRA_CODE_AUDIO = 1;
- public static final int EXTRA_CODE_VIDEO = 2;
-
- public static final int NOTIFICATION_TYPE_ERROR = 0;
- public static final int NOTIFICATION_TYPE_INFO = 1;
- public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
-
- /**
- * Receivers of this intent should update their information about the curently playing media
- */
- public static final int NOTIFICATION_TYPE_RELOAD = 3;
- /**
- * The state of the sleeptimer changed.
- */
- public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
- public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
- public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
- /**
- * No more episodes are going to be played.
- */
- public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
-
- /**
- * Playback speed has changed
- */
- public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8;
-
- /**
- * Returned by getPositionSafe() or getDurationSafe() if the playbackService
- * is in an invalid state.
- */
- public static final int INVALID_TIME = -1;
-
- /**
- * Is true if service is running.
- */
- public static boolean isRunning = false;
- /**
- * Is true if service has received a valid start command.
- */
- public static boolean started = false;
-
- private static final int NOTIFICATION_ID = 1;
-
- private RemoteControlClient remoteControlClient;
- private PlaybackServiceMediaPlayer mediaPlayer;
- private PlaybackServiceTaskManager taskManager;
-
- private static volatile MediaType currentMediaType = MediaType.UNKNOWN;
-
- private final IBinder mBinder = new LocalBinder();
-
- public class LocalBinder extends Binder {
- public PlaybackService getService() {
- return PlaybackService.this;
- }
- }
-
- @Override
- public boolean onUnbind(Intent intent) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received onUnbind event");
- return super.onUnbind(intent);
- }
-
- /**
- * Returns an intent which starts an audio- or videoplayer, depending on the
- * type of media that is being played. If the playbackservice is not
- * running, the type of the last played media will be looked up.
- */
- public static Intent getPlayerActivityIntent(Context context) {
- if (isRunning) {
- if (currentMediaType == MediaType.VIDEO) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- } else {
- if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- }
- }
-
- /**
- * Same as getPlayerActivityIntent(context), but here the type of activity
- * depends on the FeedMedia that is provided as an argument.
- */
- public static Intent getPlayerActivityIntent(Context context, Playable media) {
- MediaType mt = media.getMediaType();
- if (mt == MediaType.VIDEO) {
- return new Intent(context, VideoplayerActivity.class);
- } else {
- return new Intent(context, AudioplayerActivity.class);
- }
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onCreate() {
- super.onCreate();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Service created.");
- isRunning = true;
-
- registerReceiver(headsetDisconnected, new IntentFilter(
- Intent.ACTION_HEADSET_PLUG));
- registerReceiver(shutdownReceiver, new IntentFilter(
- ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- registerReceiver(audioBecomingNoisy, new IntentFilter(
- AudioManager.ACTION_AUDIO_BECOMING_NOISY));
- registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
- ACTION_SKIP_CURRENT_EPISODE));
- remoteControlClient = setupRemoteControlClient();
- taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
- mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback);
-
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Service is about to be destroyed");
- isRunning = false;
- started = false;
- currentMediaType = MediaType.UNKNOWN;
-
- unregisterReceiver(headsetDisconnected);
- unregisterReceiver(shutdownReceiver);
- unregisterReceiver(audioBecomingNoisy);
- unregisterReceiver(skipCurrentEpisodeReceiver);
- mediaPlayer.shutdown();
- taskManager.shutdown();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received onBind event");
- return mBinder;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- super.onStartCommand(intent, flags, startId);
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "OnStartCommand called");
- final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
- final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
- if (keycode == -1 && playable == null) {
- Log.e(TAG, "PlaybackService was started with no arguments");
- stopSelf();
- }
-
- if ((flags & Service.START_FLAG_REDELIVERY) != 0) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.");
- stopForeground(true);
- } else {
-
- if (keycode != -1) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received media button event");
- handleKeycode(keycode);
- } else {
- started = true;
- boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
- true);
- boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
- boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
- mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
- }
- }
-
- return Service.START_REDELIVER_INTENT;
- }
-
- /**
- * Handles media button events
- */
- private void handleKeycode(int keycode) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Handling keycode: " + keycode);
-
- final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
- final PlayerStatus status = info.playerStatus;
- switch (keycode) {
- case KeyEvent.KEYCODE_HEADSETHOOK:
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
- if (status == PlayerStatus.PLAYING) {
- if (UserPreferences.isPersistNotify()) {
- mediaPlayer.pause(false, true);
- }
- else {
- mediaPlayer.pause(true, true);
- }
- } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
- mediaPlayer.resume();
- } else if (status == PlayerStatus.PREPARING) {
- mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared());
- } else if (status == PlayerStatus.INITIALIZED) {
- mediaPlayer.setStartWhenPrepared(true);
- mediaPlayer.prepare();
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_PLAY:
- if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
- mediaPlayer.resume();
- } else if (status == PlayerStatus.INITIALIZED) {
- mediaPlayer.setStartWhenPrepared(true);
- mediaPlayer.prepare();
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_PAUSE:
- if (status == PlayerStatus.PLAYING) {
- if (UserPreferences.isPersistNotify()) {
- mediaPlayer.pause(false, true);
- }
- else {
- mediaPlayer.pause(true, true);
- }
- }
- break;
- case KeyEvent.KEYCODE_MEDIA_NEXT:
- case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
- mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs());
- break;
- case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
- case KeyEvent.KEYCODE_MEDIA_REWIND:
- mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs());
- break;
- case KeyEvent.KEYCODE_MEDIA_STOP:
- if (status == PlayerStatus.PLAYING) {
- mediaPlayer.pause(true, true);
- }
- stopForeground(true); // gets rid of persistent notification
- break;
- default:
- if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something
- String message = String.format(getResources().getString(R.string.unknown_media_key), keycode);
- Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
- }
- break;
- }
- }
-
- /**
- * Called by a mediaplayer Activity as soon as it has prepared its
- * mediaplayer.
- */
- public void setVideoSurface(SurfaceHolder sh) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Setting display");
- mediaPlayer.setVideoSurface(sh);
- }
-
- /**
- * Called when the surface holder of the mediaplayer has to be changed.
- */
- private void resetVideoSurface() {
- taskManager.cancelPositionSaver();
- mediaPlayer.resetVideoSurface();
- }
-
- public void notifyVideoSurfaceAbandoned() {
- stopForeground(true);
- mediaPlayer.resetVideoSurface();
- }
-
- private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
- saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL);
- }
-
- @Override
- public void onSleepTimerExpired() {
- mediaPlayer.pause(true, true);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
-
- @Override
- public void onWidgetUpdaterTick() {
- updateWidget();
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
- }
- };
-
- private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
- @Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
- currentMediaType = mediaPlayer.getCurrentMediaType();
- switch (newInfo.playerStatus) {
- case INITIALIZED:
- writePlaybackPreferences();
- break;
-
- case PREPARED:
- taskManager.startChapterLoader(newInfo.playable);
- break;
-
- case PAUSED:
- taskManager.cancelPositionSaver();
- saveCurrentPosition(false, 0);
- taskManager.cancelWidgetUpdater();
- if (UserPreferences.isPersistNotify() && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- // do not remove notification on pause based on user pref and whether android version supports expanded notifications
- }
- else {
- // remove notifcation on pause
- stopForeground(true);
- }
- break;
-
- case STOPPED:
- //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
- //stopSelf();
- break;
-
- case PLAYING:
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Audiofocus successfully requested");
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Resuming/Starting playback");
-
- taskManager.startPositionSaver();
- taskManager.startWidgetUpdater();
- setupNotification(newInfo);
- break;
- case ERROR:
- writePlaybackPreferencesNoMediaPlaying();
- break;
-
- }
-
- sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
- updateWidget();
- refreshRemoteControlClientState(newInfo);
- bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
- bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
- }
-
- @Override
- public void shouldStop() {
- stopSelf();
- }
-
- @Override
- public void playbackSpeedChanged(float s) {
- sendNotificationBroadcast(
- NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
- }
-
- @Override
- public void onBufferingUpdate(int percent) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
- }
-
- @Override
- public boolean onMediaPlayerInfo(int code) {
- switch (code) {
- case MediaPlayer.MEDIA_INFO_BUFFERING_START:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
- return true;
- case MediaPlayer.MEDIA_INFO_BUFFERING_END:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
- return true;
- default:
- return false;
- }
- }
-
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- final String TAG = "PlaybackService.onErrorListener";
- Log.w(TAG, "An error has occured: " + what + " " + extra);
- if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) {
- mediaPlayer.pause(true, false);
- }
- sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
- writePlaybackPreferencesNoMediaPlaying();
- stopSelf();
- return true;
- }
-
- @Override
- public boolean endPlayback(boolean playNextEpisode) {
- PlaybackService.this.endPlayback(true);
- return true;
- }
-
- @Override
- public RemoteControlClient getRemoteControlClient() {
- return remoteControlClient;
- }
- };
-
- private void endPlayback(boolean playNextEpisode) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Playback ended");
-
- final Playable media = mediaPlayer.getPSMPInfo().playable;
- if (media == null) {
- Log.e(TAG, "Cannot end playback: media was null");
- return;
- }
-
- taskManager.cancelPositionSaver();
-
- boolean isInQueue = false;
- FeedItem nextItem = null;
-
- if (media instanceof FeedMedia) {
- FeedItem item = ((FeedMedia) media).getItem();
- DBWriter.markItemRead(PlaybackService.this, item, true, true);
-
- try {
- final List<FeedItem> queue = taskManager.getQueue();
- isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
- nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
- } catch (InterruptedException e) {
- e.printStackTrace();
- // isInQueue remains false
- }
- if (isInQueue) {
- DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
- }
- DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
-
- // auto-flattr if enabled
- if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) {
- DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item);
- }
- }
-
- // Load next episode if previous episode was in the queue and if there
- // is an episode in the queue left.
- // Start playback immediately if continuous playback is enabled
- Playable nextMedia = null;
- boolean loadNextItem = isInQueue && nextItem != null;
- playNextEpisode = playNextEpisode && loadNextItem
- && UserPreferences.isFollowQueue();
- if (loadNextItem) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading next item in queue");
- nextMedia = nextItem.getMedia();
- }
- final boolean prepareImmediately;
- final boolean startWhenPrepared;
- final boolean stream;
-
- if (playNextEpisode) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Playback of next episode will start immediately.");
- prepareImmediately = startWhenPrepared = true;
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "No more episodes available to play");
-
- prepareImmediately = startWhenPrepared = false;
- stopForeground(true);
- stopWidgetUpdater();
- }
-
- writePlaybackPreferencesNoMediaPlaying();
- if (nextMedia != null) {
- stream = !media.localFileAvailable();
- mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
- (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
- } else {
- sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
- mediaPlayer.stop();
- //stopSelf();
- }
- }
-
- public void setSleepTimer(long waitingTime) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
- + " milliseconds");
- taskManager.setSleepTimer(waitingTime);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
- public void disableSleepTimer() {
- taskManager.disableSleepTimer();
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
- private void writePlaybackPreferencesNoMediaPlaying() {
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()).edit();
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.commit();
- }
-
-
- private void writePlaybackPreferences() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Writing playback preferences");
-
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()).edit();
- PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
- MediaType mediaType = mediaPlayer.getCurrentMediaType();
- boolean stream = mediaPlayer.isStreaming();
-
- if (info.playable != null) {
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
- info.playable.getPlayableType());
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
- stream);
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
- mediaType == MediaType.VIDEO);
- if (info.playable instanceof FeedMedia) {
- FeedMedia fMedia = (FeedMedia) info.playable;
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- fMedia.getItem().getFeed().getId());
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- fMedia.getId());
- } else {
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- }
- info.playable.writeToPreferences(editor);
- } else {
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
- PlaybackPreferences.NO_MEDIA_PLAYING);
- }
-
- editor.commit();
- }
-
- /**
- * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute.
- */
- private void postStatusUpdateIntent() {
- sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
- }
-
- private void sendNotificationBroadcast(int type, int code) {
- Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
- intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
- intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
- sendBroadcast(intent);
- }
-
- /**
- * Used by setupNotification to load notification data in another thread.
- */
- private AsyncTask<Void, Void, Void> notificationSetupTask;
-
- /**
- * Prepares notification and starts the service in the foreground.
- */
- @SuppressLint("NewApi")
- private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
- final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
- PlaybackService.getPlayerActivityIntent(this),
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- if (notificationSetupTask != null) {
- notificationSetupTask.cancel(true);
- }
- notificationSetupTask = new AsyncTask<Void, Void, Void>() {
- Bitmap icon = null;
-
- @Override
- protected Void doInBackground(Void... params) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Starting background work");
- if (android.os.Build.VERSION.SDK_INT >= 11) {
- if (info.playable != null) {
- try {
- int iconSize = getResources().getDimensionPixelSize(
- android.R.dimen.notification_large_icon_width);
- icon = PicassoProvider.getMediaMetadataPicassoInstance(PlaybackService.this)
- .load(info.playable.getImageUri())
- .resize(iconSize, iconSize)
- .get();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- }
- if (icon == null) {
- icon = BitmapFactory.decodeResource(getResources(),
- R.drawable.ic_stat_antenna);
- }
-
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
- if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING
- && info.playable != null) {
- String contentText = info.playable.getFeedTitle();
- String contentTitle = info.playable.getEpisodeTitle();
- Notification notification = null;
- if (android.os.Build.VERSION.SDK_INT >= 16) {
- Intent pauseButtonIntent = new Intent( // pause button intent
- PlaybackService.this, PlaybackService.class);
- pauseButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_PAUSE);
- PendingIntent pauseButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 0,
- pauseButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Intent playButtonIntent = new Intent( // play button intent
- PlaybackService.this, PlaybackService.class);
- playButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_PLAY);
- PendingIntent playButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 1,
- playButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Intent stopButtonIntent = new Intent( // stop button intent
- PlaybackService.this, PlaybackService.class);
- stopButtonIntent.putExtra(
- MediaButtonReceiver.EXTRA_KEYCODE,
- KeyEvent.KEYCODE_MEDIA_STOP);
- PendingIntent stopButtonPendingIntent = PendingIntent
- .getService(PlaybackService.this, 2,
- stopButtonIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- Notification.Builder notificationBuilder = new Notification.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setOngoing(true)
- .setContentIntent(pIntent)
- .setLargeIcon(icon)
- .setSmallIcon(R.drawable.ic_stat_antenna)
- .setPriority(UserPreferences.getNotifyPriority()) // set notification priority
- .addAction(android.R.drawable.ic_media_play, //play action
- getString(R.string.play_label),
- playButtonPendingIntent)
- .addAction(android.R.drawable.ic_media_pause, //pause action
- getString(R.string.pause_label),
- pauseButtonPendingIntent)
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, // stop action
- getString(R.string.stop_label),
- stopButtonPendingIntent);
- notification = notificationBuilder.build();
- } else {
- NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
- PlaybackService.this)
- .setContentTitle(contentTitle)
- .setContentText(contentText).setOngoing(true)
- .setContentIntent(pIntent).setLargeIcon(icon)
- .setSmallIcon(R.drawable.ic_stat_antenna);
- notification = notificationBuilder.getNotification();
- }
- startForeground(NOTIFICATION_ID, notification);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Notification set up");
- }
- }
-
- };
- if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
- notificationSetupTask
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- notificationSetupTask.execute();
- }
-
- }
-
- /**
- * Saves the current position of the media file to the DB
- *
- * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects
- * @param deltaPlayedDuration value by which played_duration should be increased.
- */
- private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) {
- int position = getCurrentPosition();
- int duration = getDuration();
- float playbackSpeed = getCurrentPlaybackSpeed();
- final Playable playable = mediaPlayer.getPSMPInfo().playable;
- if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Saving current position to " + position);
- if (updatePlayedDuration && playable instanceof FeedMedia) {
- FeedMedia m = (FeedMedia) playable;
- FeedItem item = m.getItem();
- m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed)));
- // Auto flattr
- if (isAutoFlattrable(m) &&
- (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) {
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration())
- + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration));
- DBTasks.flattrItemIfLoggedIn(this, item);
- }
- }
- playable.saveCurrentPosition(PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()),
- position
- );
- }
- }
-
- private void stopWidgetUpdater() {
- taskManager.cancelWidgetUpdater();
- sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE));
- }
-
- private void updateWidget() {
- PlaybackService.this.sendBroadcast(new Intent(
- PlayerWidget.FORCE_WIDGET_UPDATE));
- }
-
- public boolean sleepTimerActive() {
- return taskManager.isSleepTimerActive();
- }
-
- public long getSleepTimerTimeLeft() {
- return taskManager.getSleepTimerTimeLeft();
- }
-
- @SuppressLint("NewApi")
- private RemoteControlClient setupRemoteControlClient() {
- if (Build.VERSION.SDK_INT < 14) {
- return null;
- }
-
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- mediaButtonIntent.setComponent(new ComponentName(getPackageName(),
- MediaButtonReceiver.class.getName()));
- PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
- getApplicationContext(), 0, mediaButtonIntent, 0);
- remoteControlClient = new RemoteControlClient(mediaPendingIntent);
- int controlFlags;
- if (android.os.Build.VERSION.SDK_INT < 16) {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
- | RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
- } else {
- controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
- }
- remoteControlClient.setTransportControlFlags(controlFlags);
- return remoteControlClient;
- }
-
- /**
- * Refresh player status and metadata.
- */
- @SuppressLint("NewApi")
- private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) {
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- if (remoteControlClient != null) {
- switch (info.playerStatus) {
- case PLAYING:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
- break;
- case PAUSED:
- case INITIALIZED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
- break;
- case STOPPED:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
- break;
- case ERROR:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
- break;
- default:
- remoteControlClient
- .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
- }
- if (info.playable != null) {
- MetadataEditor editor = remoteControlClient
- .editMetadata(false);
- editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
- info.playable.getEpisodeTitle());
-
- editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
- info.playable.getFeedTitle());
-
- editor.apply();
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "RemoteControlClient state was refreshed");
- }
- }
- }
-
- private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) {
- boolean isPlaying = false;
-
- if (info.playerStatus == PlayerStatus.PLAYING) {
- isPlaying = true;
- }
-
- if (info.playable != null) {
- Intent i = new Intent(whatChanged);
- i.putExtra("id", 1);
- i.putExtra("artist", "");
- i.putExtra("album", info.playable.getFeedTitle());
- i.putExtra("track", info.playable.getEpisodeTitle());
- i.putExtra("playing", isPlaying);
- final List<FeedItem> queue = taskManager.getQueueIfLoaded();
- if (queue != null) {
- i.putExtra("ListSize", queue.size());
- }
- i.putExtra("duration", info.playable.getDuration());
- i.putExtra("position", info.playable.getPosition());
- sendBroadcast(i);
- }
- }
-
- /**
- * Pauses playback when the headset is disconnected and the preference is
- * set
- */
- private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
- private static final String TAG = "headsetDisconnected";
- private static final int UNPLUGGED = 0;
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) {
- int state = intent.getIntExtra("state", -1);
- if (state != -1) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Headset plug event. State is " + state);
- if (state == UNPLUGGED) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Headset was unplugged during playback.");
- pauseIfPauseOnDisconnect();
- }
- } else {
- Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
- }
- }
- }
- };
-
- private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- // sound is about to change, eg. bluetooth -> speaker
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Pausing playback because audio is becoming noisy");
- pauseIfPauseOnDisconnect();
- }
- // android.media.AUDIO_BECOMING_NOISY
- };
-
- /**
- * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
- */
- private void pauseIfPauseOnDisconnect() {
- if (UserPreferences.isPauseOnHeadsetDisconnect()) {
- if (UserPreferences.isPersistNotify()) {
- mediaPlayer.pause(false, true);
- }
- else {
- mediaPlayer.pause(true, true);
- }
- }
- }
-
- private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
- stopSelf();
- }
- }
-
- };
-
- private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
- mediaPlayer.endPlayback();
- }
- }
- };
-
- public static MediaType getCurrentMediaType() {
- return currentMediaType;
- }
-
- public void resume() {
- mediaPlayer.resume();
- }
-
- public void prepare() {
- mediaPlayer.prepare();
- }
-
- public void pause(boolean abandonAudioFocus, boolean reinit) {
- mediaPlayer.pause(abandonAudioFocus, reinit);
- }
-
- public void reinit() {
- mediaPlayer.reinit();
- }
-
- public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() {
- return mediaPlayer.getPSMPInfo();
- }
-
- public PlayerStatus getStatus() {
- return mediaPlayer.getPSMPInfo().playerStatus;
- }
-
- public Playable getPlayable() {
- return mediaPlayer.getPSMPInfo().playable;
- }
-
- public void setSpeed(float speed) {
- mediaPlayer.setSpeed(speed);
- }
-
- public boolean canSetSpeed() {
- return mediaPlayer.canSetSpeed();
- }
-
- public float getCurrentPlaybackSpeed() {
- return mediaPlayer.getPlaybackSpeed();
- }
-
- public boolean isStartWhenPrepared() {
- return mediaPlayer.isStartWhenPrepared();
- }
-
- public void setStartWhenPrepared(boolean s) {
- mediaPlayer.setStartWhenPrepared(s);
- }
-
-
- public void seekTo(final int t) {
- mediaPlayer.seekTo(t);
- }
-
-
- public void seekDelta(final int d) {
- mediaPlayer.seekDelta(d);
- }
-
- /**
- * @see de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.feed.Chapter)
- */
- public void seekToChapter(Chapter c) {
- mediaPlayer.seekToChapter(c);
- }
-
- /**
- * call getDuration() on mediaplayer or return INVALID_TIME if player is in
- * an invalid state.
- */
- public int getDuration() {
- return mediaPlayer.getDuration();
- }
-
- /**
- * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
- * is in an invalid state.
- */
- public int getCurrentPosition() {
- return mediaPlayer.getPosition();
- }
-
- public boolean isStreaming() {
- return mediaPlayer.isStreaming();
- }
-
- public Pair<Integer, Integer> getVideoSize() {
- return mediaPlayer.getVideoSize();
- }
-
- private boolean isAutoFlattrable(Playable p) {
- if (p != null && p instanceof FeedMedia) {
- FeedMedia media = (FeedMedia) p;
- FeedItem item = ((FeedMedia) p).getItem();
- return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred();
- } else {
- return false;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
deleted file mode 100644
index 9978fff3c..000000000
--- a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
+++ /dev/null
@@ -1,979 +0,0 @@
-package de.danoeh.antennapod.service.playback;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.AudioManager;
-import android.media.RemoteControlClient;
-import android.net.wifi.WifiManager;
-import android.os.PowerManager;
-import android.telephony.TelephonyManager;
-import android.util.Log;
-import android.util.Pair;
-import android.view.SurfaceHolder;
-
-import org.apache.commons.lang3.Validate;
-
-import java.io.IOException;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.RejectedExecutionHandler;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.ReentrantLock;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.receiver.MediaButtonReceiver;
-import de.danoeh.antennapod.util.playback.AudioPlayer;
-import de.danoeh.antennapod.util.playback.IPlayer;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.util.playback.VideoPlayer;
-
-/**
- * Manages the MediaPlayer object of the PlaybackService.
- */
-public class PlaybackServiceMediaPlayer {
- public static final String TAG = "PlaybackServiceMediaPlayer";
-
- /**
- * Return value of some PSMP methods if the method call failed.
- */
- public static final int INVALID_TIME = -1;
-
- private final AudioManager audioManager;
-
- private volatile PlayerStatus playerStatus;
- private volatile PlayerStatus statusBeforeSeeking;
- private volatile IPlayer mediaPlayer;
- private volatile Playable media;
-
- private volatile boolean stream;
- private volatile MediaType mediaType;
- private volatile AtomicBoolean startWhenPrepared;
- private volatile boolean pausedBecauseOfTransientAudiofocusLoss;
- private volatile Pair<Integer, Integer> videoSize;
-
- /**
- * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads
- * have to wait until these operations have finished.
- */
- private final ReentrantLock playerLock;
-
- private final PSMPCallback callback;
- private final Context context;
-
- private final ThreadPoolExecutor executor;
-
- /**
- * A wifi-lock that is acquired if the media file is being streamed.
- */
- private WifiManager.WifiLock wifiLock;
-
- public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) {
- Validate.notNull(context);
- Validate.notNull(callback);
-
- this.context = context;
- this.callback = callback;
- this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- this.playerLock = new ReentrantLock();
- this.startWhenPrepared = new AtomicBoolean(false);
- executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(),
- new RejectedExecutionHandler() {
- @Override
- public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable");
- }
- }
- );
-
- mediaPlayer = null;
- statusBeforeSeeking = null;
- pausedBecauseOfTransientAudiofocusLoss = false;
- mediaType = MediaType.UNKNOWN;
- playerStatus = PlayerStatus.STOPPED;
- videoSize = null;
- }
-
- /**
- * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
- * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
- * not do anything.
- * Whether playback starts immediately depends on the given parameters. See below for more details.
- * <p/>
- * States:
- * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
- * <p/>
- * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
- * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
- * <p/>
- * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
- * will enter the ERROR state.
- * <p/>
- * This method is executed on an internal executor service.
- *
- * @param playable The Playable object that is supposed to be played. This parameter must not be null.
- * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
- * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
- * the Android MediaPlayer via getStreamUrl.
- * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the
- * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared
- * for playback immediately (see 'prepareImmediately' parameter for more details)
- * @param prepareImmediately Set to true if the method should also prepare the episode for playback.
- */
- public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- Validate.notNull(playable);
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Play media object.");
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- try {
- playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
- } catch (RuntimeException e) {
- e.printStackTrace();
- throw e;
- } finally {
- playerLock.unlock();
- }
- }
- });
- }
-
- /**
- * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
- * the given playable parameter is the same object as the currently playing media.
- * <p/>
- * This method requires the playerLock and is executed on the caller's thread.
- *
- * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean)
- */
- private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- Validate.notNull(playable);
- if (!playerLock.isHeldByCurrentThread())
- throw new IllegalStateException("method requires playerLock");
-
-
- if (media != null) {
- if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) {
- // episode is already playing -> ignore method call
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
- return;
- } else {
- // stop playback of this episode
- if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) {
- mediaPlayer.stop();
- }
- setPlayerStatus(PlayerStatus.INDETERMINATE, null);
- }
- }
-
- this.media = playable;
- this.stream = stream;
- this.mediaType = media.getMediaType();
- this.videoSize = null;
- createMediaPlayer();
- PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared);
- setPlayerStatus(PlayerStatus.INITIALIZING, media);
- try {
- media.loadMetadata();
- if (stream) {
- mediaPlayer.setDataSource(media.getStreamUrl());
- } else {
- mediaPlayer.setDataSource(media.getLocalMediaUrl());
- }
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
-
- if (mediaType == MediaType.VIDEO) {
- VideoPlayer vp = (VideoPlayer) mediaPlayer;
- // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
- }
-
- if (prepareImmediately) {
- setPlayerStatus(PlayerStatus.PREPARING, media);
- mediaPlayer.prepare();
- onPrepared(startWhenPrepared);
- }
-
- } catch (Playable.PlayableException e) {
- e.printStackTrace();
- setPlayerStatus(PlayerStatus.ERROR, null);
- } catch (IOException e) {
- e.printStackTrace();
- setPlayerStatus(PlayerStatus.ERROR, null);
- } catch (IllegalStateException e) {
- e.printStackTrace();
- setPlayerStatus(PlayerStatus.ERROR, null);
- }
- }
-
-
- /**
- * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
- * nothing will happen.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public void resume() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- resumeSync();
- playerLock.unlock();
- }
- });
- }
-
- private void resumeSync() {
- if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
- int focusGained = audioManager.requestAudioFocus(
- audioFocusChangeListener, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
- if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- acquireWifiLockIfNecessary();
- setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
- mediaPlayer.start();
- if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
- mediaPlayer.seekTo(media.getPosition());
- }
-
- setPlayerStatus(PlayerStatus.PLAYING, media);
- pausedBecauseOfTransientAudiofocusLoss = false;
- if (android.os.Build.VERSION.SDK_INT >= 14) {
- RemoteControlClient remoteControlClient = callback.getRemoteControlClient();
- if (remoteControlClient != null) {
- audioManager
- .registerRemoteControlClient(remoteControlClient);
- }
- }
- audioManager
- .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(),
- MediaButtonReceiver.class.getName()));
- media.onPlaybackStart();
-
- } else {
- if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus");
- }
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus);
- }
- }
-
-
- /**
- * Saves the current position and pauses playback. Note that, if audiofocus
- * is abandoned, the lockscreen controls will also disapear.
- * <p/>
- * This method is executed on an internal executor service.
- *
- * @param abandonFocus is true if the service should release audio focus
- * @param reinit is true if service should reinit after pausing if the media
- * file is being streamed
- */
- public void pause(final boolean abandonFocus, final boolean reinit) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- releaseWifiLockIfNecessary();
- if (playerStatus == PlayerStatus.PLAYING) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Pausing playback.");
- mediaPlayer.pause();
- setPlayerStatus(PlayerStatus.PAUSED, media);
-
- if (abandonFocus) {
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- pausedBecauseOfTransientAudiofocusLoss = false;
- }
- if (stream && reinit) {
- reinit();
- }
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
- }
-
- playerLock.unlock();
- }
- });
- }
-
- /**
- * Prepared media player for playback if the service is in the INITALIZED
- * state.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public void prepare() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
-
- if (playerStatus == PlayerStatus.INITIALIZED) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Preparing media player");
- setPlayerStatus(PlayerStatus.PREPARING, media);
- try {
- mediaPlayer.prepare();
- onPrepared(startWhenPrepared.get());
- } catch (IOException e) {
- e.printStackTrace();
- setPlayerStatus(PlayerStatus.ERROR, null);
- }
- }
- playerLock.unlock();
-
- }
- });
- }
-
- /**
- * Called after media player has been prepared. This method is executed on the caller's thread.
- */
- void onPrepared(final boolean startWhenPrepared) {
- playerLock.lock();
-
- if (playerStatus != PlayerStatus.PREPARING) {
- playerLock.unlock();
- throw new IllegalStateException("Player is not in PREPARING state");
- }
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Resource prepared");
-
- if (mediaType == MediaType.VIDEO) {
- VideoPlayer vp = (VideoPlayer) mediaPlayer;
- videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight());
- }
-
- if (media.getPosition() > 0) {
- mediaPlayer.seekTo(media.getPosition());
- }
-
- if (media.getDuration() == 0) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Setting duration of media");
- media.setDuration(mediaPlayer.getDuration());
- }
- setPlayerStatus(PlayerStatus.PREPARED, media);
-
- if (startWhenPrepared) {
- resumeSync();
- }
-
- playerLock.unlock();
- }
-
- /**
- * Resets the media player and moves it into INITIALIZED state.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public void reinit() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- releaseWifiLockIfNecessary();
- if (media != null) {
- playMediaObject(media, true, stream, startWhenPrepared.get(), false);
- } else if (mediaPlayer != null) {
- mediaPlayer.reset();
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
- }
- playerLock.unlock();
- }
- });
- }
-
-
- /**
- * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
- *
- * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0
- * <p/>
- * This method is executed on the caller's thread.
- */
- private void seekToSync(int t) {
- if (t < 0) {
- t = 0;
- }
- playerLock.lock();
-
- if (playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED) {
- if (stream) {
- // statusBeforeSeeking = playerStatus;
- // setPlayerStatus(PlayerStatus.SEEKING, media);
- }
- mediaPlayer.seekTo(t);
-
- } else if (playerStatus == PlayerStatus.INITIALIZED) {
- media.setPosition(t);
- startWhenPrepared.set(true);
- prepare();
- }
- playerLock.unlock();
- }
-
- /**
- * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
- * Invalid time values (< 0) will be ignored.
- * <p/>
- * This method is executed on an internal executor service.
- */
- public void seekTo(final int t) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- seekToSync(t);
- }
- });
- }
-
- /**
- * Seek a specific position from the current position
- *
- * @param d offset from current position (positive or negative)
- */
- public void seekDelta(final int d) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- int currentPosition = getPosition();
- if (currentPosition != INVALID_TIME) {
- seekToSync(currentPosition + d);
- } else {
- Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
- }
-
- playerLock.unlock();
- }
- });
- }
-
- /**
- * Seek to the start of the specified chapter.
- */
- public void seekToChapter(Chapter c) {
- Validate.notNull(c);
-
- seekTo((int) c.getStart());
- }
-
- /**
- * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
- */
- public int getDuration() {
- if (!playerLock.tryLock()) {
- return INVALID_TIME;
- }
-
- int retVal = INVALID_TIME;
- if (playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED) {
- retVal = mediaPlayer.getDuration();
- } else if (media != null && media.getDuration() > 0) {
- retVal = media.getDuration();
- }
-
- playerLock.unlock();
- return retVal;
- }
-
- /**
- * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
- */
- public int getPosition() {
- if (!playerLock.tryLock()) {
- return INVALID_TIME;
- }
-
- int retVal = INVALID_TIME;
- if (playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED) {
- retVal = mediaPlayer.getCurrentPosition();
- } else if (media != null && media.getPosition() > 0) {
- retVal = media.getPosition();
- }
-
- playerLock.unlock();
- return retVal;
- }
-
- public boolean isStartWhenPrepared() {
- return startWhenPrepared.get();
- }
-
- public void setStartWhenPrepared(boolean startWhenPrepared) {
- this.startWhenPrepared.set(startWhenPrepared);
- }
-
- /**
- * Returns true if the playback speed can be adjusted.
- */
- public boolean canSetSpeed() {
- boolean retVal = false;
- if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
- retVal = (mediaPlayer).canSetSpeed();
- }
- return retVal;
- }
-
- /**
- * Sets the playback speed.
- * This method is executed on the caller's thread.
- */
- private void setSpeedSync(float speed) {
- playerLock.lock();
- if (media != null && media.getMediaType() == MediaType.AUDIO) {
- if (mediaPlayer.canSetSpeed()) {
- mediaPlayer.setPlaybackSpeed((float) speed);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Playback speed was set to " + speed);
- callback.playbackSpeedChanged(speed);
- }
- }
- playerLock.unlock();
- }
-
- /**
- * Sets the playback speed.
- * This method is executed on an internal executor service.
- */
- public void setSpeed(final float speed) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- setSpeedSync(speed);
- }
- });
- }
-
- /**
- * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
- */
- public float getPlaybackSpeed() {
- if (!playerLock.tryLock()) {
- return 1;
- }
-
- float retVal = 1;
- if ((playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) {
- retVal = mediaPlayer.getCurrentSpeedMultiplier();
- }
- playerLock.unlock();
- return retVal;
- }
-
- public MediaType getCurrentMediaType() {
- return mediaType;
- }
-
- public boolean isStreaming() {
- return stream;
- }
-
-
- /**
- * Releases internally used resources. This method should only be called when the object is not used anymore.
- */
- public void shutdown() {
- executor.shutdown();
- if (mediaPlayer != null) {
- mediaPlayer.release();
- }
- releaseWifiLockIfNecessary();
- }
-
- public void setVideoSurface(final SurfaceHolder surface) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- if (mediaPlayer != null) {
- mediaPlayer.setDisplay(surface);
- }
- playerLock.unlock();
- }
- });
- }
-
- public void resetVideoSurface() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Resetting video surface");
- mediaPlayer.setDisplay(null);
- reinit();
- playerLock.unlock();
- }
- });
- }
-
- /**
- * Return width and height of the currently playing video as a pair.
- *
- * @return Width and height as a Pair or null if the video size could not be determined. The method might still
- * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
- * invalid values.
- */
- public Pair<Integer, Integer> getVideoSize() {
- if (!playerLock.tryLock()) {
- // use cached value if lock can't be aquired
- return videoSize;
- }
- Pair<Integer, Integer> res;
- if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) {
- res = null;
- } else {
- VideoPlayer vp = (VideoPlayer) mediaPlayer;
- videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight());
- res = videoSize;
- }
- playerLock.unlock();
- return res;
- }
-
- /**
- * Returns a PSMInfo object that contains information about the current state of the PSMP object.
- *
- * @return The PSMPInfo object.
- */
- public synchronized PSMPInfo getPSMPInfo() {
- return new PSMPInfo(playerStatus, media);
- }
-
- /**
- * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
- * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
- * <p/>
- * This method will notify the callback about the change of the player status (even if the new status is the same
- * as the old one).
- *
- * @param newStatus The new PlayerStatus. This must not be null.
- * @param newMedia The new playable object of the PSMP object. This can be null.
- */
- private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) {
- Validate.notNull(newStatus);
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus);
-
- this.playerStatus = newStatus;
- this.media = newMedia;
- callback.statusChanged(new PSMPInfo(playerStatus, media));
- }
-
- private IPlayer createMediaPlayer() {
- if (mediaPlayer != null) {
- mediaPlayer.release();
- }
- if (media == null || media.getMediaType() == MediaType.VIDEO) {
- mediaPlayer = new VideoPlayer();
- } else {
- mediaPlayer = new AudioPlayer(context);
- }
- mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
- return setMediaPlayerListeners(mediaPlayer);
- }
-
- private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
-
- @Override
- public void onAudioFocusChange(final int focusChange) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
-
- // If there is an incoming call, playback should be paused permanently
- TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- final int callState = (tm != null) ? tm.getCallState() : 0;
- if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState);
- Log.i(TAG, "Call state:" + callState);
-
- if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Lost audio focus");
- pause(true, false);
- callback.shouldStop();
- } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Gained audio focus");
- if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now
- resume();
- } else { // we ducked => raise audio level back
- audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_RAISE, 0);
- }
- } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
- if (playerStatus == PlayerStatus.PLAYING) {
- if (!UserPreferences.shouldPauseForFocusLoss()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Ducking...");
- audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_LOWER, 0);
- pausedBecauseOfTransientAudiofocusLoss = false;
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing...");
- pause(false, false);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- }
- } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
- if (playerStatus == PlayerStatus.PLAYING) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Lost audio focus temporarily. Pausing...");
- pause(false, false);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- }
- playerLock.unlock();
- }
- });
- }
- };
-
-
- public void endPlayback() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- releaseWifiLockIfNecessary();
-
- if (playerStatus != PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.INDETERMINATE, media);
- }
- if (mediaPlayer != null) {
- mediaPlayer.reset();
-
- }
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- callback.endPlayback(true);
-
- playerLock.unlock();
- }
- });
- }
-
- /**
- * Moves the PlaybackServiceMediaPlayer into STOPPED state. This call is only valid if the player is currently in
- * INDETERMINATE state, for example after a call to endPlayback.
- * This method will only take care of changing the PlayerStatus of this object! Other tasks like
- * abandoning audio focus have to be done with other methods.
- */
- public void stop() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- releaseWifiLockIfNecessary();
-
- if (playerStatus == PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.STOPPED, null);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
- }
- playerLock.unlock();
-
- }
- });
- }
-
- private synchronized void acquireWifiLockIfNecessary() {
- if (stream) {
- if (wifiLock == null) {
- wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
- .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
- wifiLock.setReferenceCounted(false);
- }
- wifiLock.acquire();
- }
- }
-
- private synchronized void releaseWifiLockIfNecessary() {
- if (wifiLock != null && wifiLock.isHeld()) {
- wifiLock.release();
- }
- }
-
- /**
- * Holds information about a PSMP object.
- */
- public class PSMPInfo {
- public PlayerStatus playerStatus;
- public Playable playable;
-
- public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
- this.playerStatus = playerStatus;
- this.playable = playable;
- }
- }
-
- public static interface PSMPCallback {
- public void statusChanged(PSMPInfo newInfo);
-
- public void shouldStop();
-
- public void playbackSpeedChanged(float s);
-
- public void onBufferingUpdate(int percent);
-
- public boolean onMediaPlayerInfo(int code);
-
- public boolean onMediaPlayerError(Object inObj, int what, int extra);
-
- public boolean endPlayback(boolean playNextEpisode);
-
- public RemoteControlClient getRemoteControlClient();
- }
-
- private IPlayer setMediaPlayerListeners(IPlayer mp) {
- if (mp != null && media != null) {
- if (media.getMediaType() == MediaType.AUDIO) {
- ((AudioPlayer) mp)
- .setOnCompletionListener(audioCompletionListener);
- ((AudioPlayer) mp)
- .setOnSeekCompleteListener(audioSeekCompleteListener);
- ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
- ((AudioPlayer) mp)
- .setOnBufferingUpdateListener(audioBufferingUpdateListener);
- ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
- } else {
- ((VideoPlayer) mp)
- .setOnCompletionListener(videoCompletionListener);
- ((VideoPlayer) mp)
- .setOnSeekCompleteListener(videoSeekCompleteListener);
- ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
- ((VideoPlayer) mp)
- .setOnBufferingUpdateListener(videoBufferingUpdateListener);
- ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
- }
- }
- return mp;
- }
-
- private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() {
- @Override
- public void onCompletion(com.aocate.media.MediaPlayer mp) {
- genericOnCompletion();
- }
- };
-
- private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() {
- @Override
- public void onCompletion(android.media.MediaPlayer mp) {
- genericOnCompletion();
- }
- };
-
- private void genericOnCompletion() {
- endPlayback();
- }
-
- private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() {
- @Override
- public void onBufferingUpdate(com.aocate.media.MediaPlayer mp,
- int percent) {
- genericOnBufferingUpdate(percent);
- }
- };
-
- private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() {
- @Override
- public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) {
- genericOnBufferingUpdate(percent);
- }
- };
-
- private void genericOnBufferingUpdate(int percent) {
- callback.onBufferingUpdate(percent);
- }
-
- private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() {
- @Override
- public boolean onInfo(com.aocate.media.MediaPlayer mp, int what,
- int extra) {
- return genericInfoListener(what);
- }
- };
-
- private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() {
- @Override
- public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) {
- return genericInfoListener(what);
- }
- };
-
- private boolean genericInfoListener(int what) {
- return callback.onMediaPlayerInfo(what);
- }
-
- private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(com.aocate.media.MediaPlayer mp, int what,
- int extra) {
- return genericOnError(mp, what, extra);
- }
- };
-
- private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(android.media.MediaPlayer mp, int what, int extra) {
- return genericOnError(mp, what, extra);
- }
- };
-
- private boolean genericOnError(Object inObj, int what, int extra) {
- return callback.onMediaPlayerError(inObj, what, extra);
- }
-
- private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() {
- @Override
- public void onSeekComplete(com.aocate.media.MediaPlayer mp) {
- genericSeekCompleteListener();
- }
- };
-
- private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() {
- @Override
- public void onSeekComplete(android.media.MediaPlayer mp) {
- genericSeekCompleteListener();
- }
- };
-
- private final void genericSeekCompleteListener() {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
- if (playerStatus == PlayerStatus.SEEKING) {
- setPlayerStatus(statusBeforeSeeking, media);
- }
- playerLock.unlock();
- }
- });
- }
-}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
deleted file mode 100644
index 680ec2e2f..000000000
--- a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
+++ /dev/null
@@ -1,384 +0,0 @@
-package de.danoeh.antennapod.service.playback;
-
-import android.content.Context;
-import android.util.Log;
-
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.util.playback.Playable;
-
-import java.util.List;
-import java.util.concurrent.*;
-
-/**
- * Manages the background tasks of PlaybackSerivce, i.e.
- * the sleep timer, the position saver, the widget updater and
- * the queue loader.
- * <p/>
- * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback)
- * to notify the PlaybackService about updates from the running tasks.
- */
-public class PlaybackServiceTaskManager {
- private static final String TAG = "PlaybackServiceTaskManager";
-
- /**
- * Update interval of position saver in milliseconds.
- */
- public static final int POSITION_SAVER_WAITING_INTERVAL = 5000;
- /**
- * Notification interval of widget updater in milliseconds.
- */
- public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500;
-
- private static final int SCHED_EX_POOL_SIZE = 2;
- private final ScheduledThreadPoolExecutor schedExecutor;
-
- private ScheduledFuture positionSaverFuture;
- private ScheduledFuture widgetUpdaterFuture;
- private ScheduledFuture sleepTimerFuture;
- private volatile Future<List<FeedItem>> queueFuture;
- private volatile Future chapterLoaderFuture;
-
- private SleepTimer sleepTimer;
-
- private final Context context;
- private final PSTMCallback callback;
-
- /**
- * Sets up a new PSTM. This method will also start the queue loader task.
- *
- * @param context
- * @param callback A PSTMCallback object for notifying the user about updates. Must not be null.
- */
- public PlaybackServiceTaskManager(Context context, PSTMCallback callback) {
- Validate.notNull(context);
- Validate.notNull(callback);
-
- this.context = context;
- this.callback = callback;
- schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() {
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- });
- loadQueue();
- EventDistributor.getInstance().register(eventDistributorListener);
- }
-
- private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
- @Override
- public void update(EventDistributor eventDistributor, Integer arg) {
- if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
- cancelQueueLoader();
- loadQueue();
- }
- }
- };
-
- private synchronized boolean isQueueLoaderActive() {
- return queueFuture != null && !queueFuture.isDone();
- }
-
- private synchronized void cancelQueueLoader() {
- if (isQueueLoaderActive()) {
- queueFuture.cancel(true);
- }
- }
-
- private synchronized void loadQueue() {
- if (!isQueueLoaderActive()) {
- queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() {
- @Override
- public List<FeedItem> call() throws Exception {
- return DBReader.getQueue(context);
- }
- });
- }
- }
-
- /**
- * Returns the queue if it is already loaded or null if it hasn't been loaded yet.
- * In order to wait until the queue has been loaded, use getQueue()
- */
- public synchronized List<FeedItem> getQueueIfLoaded() {
- if (queueFuture.isDone()) {
- try {
- return queueFuture.get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
- return null;
- }
-
- /**
- * Returns the queue or waits until the PSTM has loaded the queue from the database.
- */
- public synchronized List<FeedItem> getQueue() throws InterruptedException {
- try {
- return queueFuture.get();
- } catch (ExecutionException e) {
- throw new IllegalArgumentException(e);
- }
- }
-
- /**
- * Starts the position saver task. If the position saver is already active, nothing will happen.
- */
- public synchronized void startPositionSaver() {
- if (!isPositionSaverActive()) {
- Runnable positionSaver = new Runnable() {
- @Override
- public void run() {
- callback.positionSaverTick();
- }
- };
- positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL,
- POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS);
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver");
- } else {
- if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored.");
- }
- }
-
- /**
- * Returns true if the position saver is currently running.
- */
- public synchronized boolean isPositionSaverActive() {
- return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone();
- }
-
- /**
- * Cancels the position saver. If the position saver is not running, nothing will happen.
- */
- public synchronized void cancelPositionSaver() {
- if (isPositionSaverActive()) {
- positionSaverFuture.cancel(false);
- if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver");
- }
- }
-
- /**
- * Starts the widget updater task. If the widget updater is already active, nothing will happen.
- */
- public synchronized void startWidgetUpdater() {
- if (!isWidgetUpdaterActive()) {
- Runnable widgetUpdater = new Runnable() {
- @Override
- public void run() {
- callback.onWidgetUpdaterTick();
- }
- };
- widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
- WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater");
- } else {
- if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored.");
- }
- }
-
- /**
- * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
- * cancelled first.
- * After waitingTime has elapsed, onSleepTimerExpired() will be called.
- *
- * @throws java.lang.IllegalArgumentException if waitingTime <= 0
- */
- public synchronized void setSleepTimer(long waitingTime) {
- Validate.isTrue(waitingTime > 0, "Waiting time <= 0");
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
- + " milliseconds");
- if (isSleepTimerActive()) {
- sleepTimerFuture.cancel(true);
- }
- sleepTimer = new SleepTimer(waitingTime);
- sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS);
- }
-
- /**
- * Returns true if the sleep timer is currently active.
- */
- public synchronized boolean isSleepTimerActive() {
- return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting;
- }
-
- /**
- * Disables the sleep timer. If the sleep timer is not active, nothing will happen.
- */
- public synchronized void disableSleepTimer() {
- if (isSleepTimerActive()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Disabling sleep timer");
- sleepTimerFuture.cancel(true);
- }
- }
-
- /**
- * Returns the current sleep timer time or 0 if the sleep timer is not active.
- */
- public synchronized long getSleepTimerTimeLeft() {
- if (isSleepTimerActive()) {
- return sleepTimer.getWaitingTime();
- } else {
- return 0;
- }
- }
-
-
- /**
- * Returns true if the widget updater is currently running.
- */
- public synchronized boolean isWidgetUpdaterActive() {
- return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone();
- }
-
- /**
- * Cancels the widget updater. If the widget updater is not running, nothing will happen.
- */
- public synchronized void cancelWidgetUpdater() {
- if (isWidgetUpdaterActive()) {
- widgetUpdaterFuture.cancel(false);
- if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater");
- }
- }
-
- private synchronized void cancelChapterLoader() {
- if (isChapterLoaderActive()) {
- chapterLoaderFuture.cancel(true);
- }
- }
-
- private synchronized boolean isChapterLoaderActive() {
- return chapterLoaderFuture != null && !chapterLoaderFuture.isDone();
- }
-
- /**
- * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active,
- * it will be cancelled first.
- * On completion, the callback's onChapterLoaded method will be called.
- */
- public synchronized void startChapterLoader(final Playable media) {
- Validate.notNull(media);
-
- if (isChapterLoaderActive()) {
- cancelChapterLoader();
- }
-
- Runnable chapterLoader = new Runnable() {
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Chapter loader started");
- if (media.getChapters() == null) {
- media.loadChapterMarks();
- if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) {
- callback.onChapterLoaded(media);
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Chapter loader stopped");
- }
- };
- chapterLoaderFuture = schedExecutor.submit(chapterLoader);
- }
-
-
- /**
- * Cancels all tasks. The PSTM will be in the initial state after execution of this method.
- */
- public synchronized void cancelAllTasks() {
- cancelPositionSaver();
- cancelWidgetUpdater();
- disableSleepTimer();
- cancelQueueLoader();
- cancelChapterLoader();
- }
-
- /**
- * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after
- * execution of this method.
- */
- public synchronized void shutdown() {
- EventDistributor.getInstance().unregister(eventDistributorListener);
- cancelAllTasks();
- schedExecutor.shutdown();
- }
-
- /**
- * Sleeps for a given time and then pauses playback.
- */
- private class SleepTimer implements Runnable {
- private static final String TAG = "SleepTimer";
- private static final long UPDATE_INTERVALL = 1000L;
- private volatile long waitingTime;
- private volatile boolean isWaiting;
-
- public SleepTimer(long waitingTime) {
- super();
- this.waitingTime = waitingTime;
- isWaiting = true;
- }
-
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Starting");
- while (waitingTime > 0) {
- try {
- Thread.sleep(UPDATE_INTERVALL);
- waitingTime -= UPDATE_INTERVALL;
-
- if (waitingTime <= 0) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Waiting completed");
- postExecute();
- if (!Thread.currentThread().isInterrupted()) {
- callback.onSleepTimerExpired();
- }
-
- }
- } catch (InterruptedException e) {
- Log.d(TAG, "Thread was interrupted while waiting");
- break;
- }
- }
- postExecute();
- }
-
- protected void postExecute() {
- isWaiting = false;
- }
-
- public long getWaitingTime() {
- return waitingTime;
- }
-
- public boolean isWaiting() {
- return isWaiting;
- }
-
- }
-
- public static interface PSTMCallback {
- void positionSaverTick();
-
- void onSleepTimerExpired();
-
- void onWidgetUpdaterTick();
-
- void onChapterLoaded(Playable media);
- }
-}
diff --git a/src/de/danoeh/antennapod/service/playback/PlayerStatus.java b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java
deleted file mode 100644
index 3d2b4ad39..000000000
--- a/src/de/danoeh/antennapod/service/playback/PlayerStatus.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.danoeh.antennapod.service.playback;
-
-public enum PlayerStatus {
- INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state.
- ERROR,
- PREPARING,
- PAUSED,
- PLAYING,
- STOPPED,
- PREPARED,
- SEEKING,
- INITIALIZING, // playback service is loading the Playable's metadata
- INITIALIZED // playback service was started, data source of media player was set.
-}
diff --git a/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
deleted file mode 100644
index ec28724ed..000000000
--- a/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package de.danoeh.antennapod.service.playback;
-
-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.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.receiver.MediaButtonReceiver;
-import de.danoeh.antennapod.receiver.PlayerWidget;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java
deleted file mode 100644
index e49ea4f83..000000000
--- a/src/de/danoeh/antennapod/storage/DBReader.java
+++ /dev/null
@@ -1,908 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.*;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.util.DownloadError;
-import de.danoeh.antennapod.util.comparator.DownloadStatusComparator;
-import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator;
-import de.danoeh.antennapod.util.comparator.PlaybackCompletionDateComparator;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-import de.danoeh.antennapod.util.flattr.FlattrThing;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-
-/**
- * Provides methods for reading data from the AntennaPod database.
- * In general, all database calls in DBReader-methods are executed on the caller's thread.
- * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread.
- * This class will use the {@link de.danoeh.antennapod.feed.EventDistributor} to notify listeners about changes in the database.
- */
-public final class DBReader {
- private static final String TAG = "DBReader";
-
- /**
- * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}.
- */
- public static final int PLAYBACK_HISTORY_SIZE = 50;
-
- /**
- * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}.
- */
- public static final int DOWNLOAD_LOG_SIZE = 200;
-
-
- private DBReader() {
- }
-
- /**
- * Returns a list of Feeds, sorted alphabetically by their title.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of Feeds, sorted alphabetically by their title. A Feed-object
- * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list
- * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)}.
- */
- public static List<Feed> getFeedList(final Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting Feedlist");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- List<Feed> result = getFeedList(adapter);
- adapter.close();
- return result;
- }
-
- private static List<Feed> getFeedList(PodDBAdapter adapter) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting Feedlist");
-
- Cursor feedlistCursor = adapter.getAllFeedsCursor();
- List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount());
-
- if (feedlistCursor.moveToFirst()) {
- do {
- Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor);
- feeds.add(feed);
- } while (feedlistCursor.moveToNext());
- }
- feedlistCursor.close();
- return feeds;
- }
-
- /**
- * Returns a list with the download URLs of all feeds.
- *
- * @param context A context that is used for opening the database connection.
- * @return A list of Strings with the download URLs of all feeds.
- */
- public static List<String> getFeedListDownloadUrls(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- List<String> result = new ArrayList<String>();
- adapter.open();
- Cursor feeds = adapter.getFeedCursorDownloadUrls();
- if (feeds.moveToFirst()) {
- do {
- result.add(feeds.getString(1));
- } while (feeds.moveToNext());
- }
- feeds.close();
- adapter.close();
-
- return result;
- }
-
- /**
- * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time.
- *
- * @param context A context that is used for opening a database connection.
- * @param expirationTime Time that is used for determining whether a feed is outdated or not.
- * A Feed is considered expired if 'lastUpdate < (currentTime - expirationTime)' evaluates to true.
- * @return A list of Feeds, sorted alphabetically by their title. A Feed-object
- * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list
- * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)}.
- */
- public static List<Feed> getExpiredFeedsList(final Context context, final long expirationTime) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, String.format("getExpiredFeedsList(%d)", expirationTime));
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
-
- Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime);
- List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount());
-
- if (feedlistCursor.moveToFirst()) {
- do {
- Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor);
- feeds.add(feed);
- } while (feedlistCursor.moveToNext());
- }
- feedlistCursor.close();
- return feeds;
- }
-
- /**
- * Takes a list of FeedItems and loads their corresponding Feed-objects from the database.
- * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will
- * not find the correct feed of an item.
- *
- * @param context A context that is used for opening a database connection.
- * @param items The FeedItems whose Feed-objects should be loaded.
- */
- public static void loadFeedDataOfFeedItemlist(Context context,
- List<FeedItem> items) {
- List<Feed> feeds = getFeedList(context);
- for (FeedItem item : items) {
- for (Feed feed : feeds) {
- if (feed.getId() == item.getFeedId()) {
- item.setFeed(feed);
- break;
- }
- }
- if (item.getFeed() == null) {
- Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId());
- }
- }
- }
-
- /**
- * Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not
- * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead.
- *
- * @param context A context that is used for opening a database connection.
- * @param feed The Feed whose items should be loaded
- * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly.
- * The method does NOT change the items-attribute of the feed.
- */
- public static List<FeedItem> getFeedItemList(Context context,
- final Feed feed) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle());
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
-
- Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed);
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
- itemlistCursor.close();
-
- Collections.sort(items, new FeedItemPubdateComparator());
-
- adapter.close();
-
- for (FeedItem item : items) {
- item.setFeed(feed);
- }
-
- return items;
- }
-
- static List<FeedItem> extractItemlistFromCursor(Context context, Cursor itemlistCursor) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- List<FeedItem> result = extractItemlistFromCursor(adapter, itemlistCursor);
- adapter.close();
- return result;
- }
-
- private static List<FeedItem> extractItemlistFromCursor(
- PodDBAdapter adapter, Cursor itemlistCursor) {
- ArrayList<String> itemIds = new ArrayList<String>();
- List<FeedItem> items = new ArrayList<FeedItem>(
- itemlistCursor.getCount());
-
- if (itemlistCursor.moveToFirst()) {
- do {
- FeedItem item = new FeedItem();
-
- item.setId(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID));
- item.setTitle(itemlistCursor
- .getString(PodDBAdapter.IDX_FI_SMALL_TITLE));
- item.setLink(itemlistCursor
- .getString(PodDBAdapter.IDX_FI_SMALL_LINK));
- item.setPubDate(new Date(itemlistCursor
- .getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE)));
- item.setPaymentLink(itemlistCursor
- .getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK));
- item.setFeedId(itemlistCursor
- .getLong(PodDBAdapter.IDX_FI_SMALL_FEED));
- itemIds.add(String.valueOf(item.getId()));
-
- item.setRead((itemlistCursor
- .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0));
- item.setItemIdentifier(itemlistCursor
- .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER));
- item.setFlattrStatus(new FlattrStatus(itemlistCursor
- .getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS)));
-
- long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE);
- if (imageIndex != 0) {
- item.setImage(getFeedImage(adapter, imageIndex));
- }
-
- // extract chapters
- boolean hasSimpleChapters = itemlistCursor
- .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0;
- if (hasSimpleChapters) {
- Cursor chapterCursor = adapter
- .getSimpleChaptersOfFeedItemCursor(item);
- if (chapterCursor.moveToFirst()) {
- item.setChapters(new ArrayList<Chapter>());
- do {
- int chapterType = chapterCursor
- .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX);
- Chapter chapter = null;
- long start = chapterCursor
- .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX);
- String title = chapterCursor
- .getString(PodDBAdapter.KEY_TITLE_INDEX);
- String link = chapterCursor
- .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX);
-
- switch (chapterType) {
- case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
- chapter = new SimpleChapter(start, title, item,
- link);
- break;
- case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
- chapter = new ID3Chapter(start, title, item,
- link);
- break;
- case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
- chapter = new VorbisCommentChapter(start,
- title, item, link);
- break;
- }
- if (chapter != null) {
- chapter.setId(chapterCursor
- .getLong(PodDBAdapter.KEY_ID_INDEX));
- item.getChapters().add(chapter);
- }
- } while (chapterCursor.moveToNext());
- }
- chapterCursor.close();
- }
- items.add(item);
- } while (itemlistCursor.moveToNext());
- }
-
- extractMediafromItemlist(adapter, items, itemIds);
- return items;
- }
-
- private static void extractMediafromItemlist(PodDBAdapter adapter,
- List<FeedItem> items, ArrayList<String> itemIds) {
-
- List<FeedItem> itemsCopy = new ArrayList<FeedItem>(items);
- Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds
- .toArray(new String[itemIds.size()]));
- if (cursor.moveToFirst()) {
- do {
- long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX);
- // find matching feed item
- FeedItem item = getMatchingItemForMedia(itemId, itemsCopy);
- if (item != null) {
- item.setMedia(extractFeedMediaFromCursorRow(cursor));
- item.getMedia().setItem(item);
- }
- } while (cursor.moveToNext());
- cursor.close();
- }
- }
-
- private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) {
- long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- Date playbackCompletionDate = null;
- long playbackCompletionTime = cursor
- .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX);
- if (playbackCompletionTime > 0) {
- playbackCompletionDate = new Date(
- playbackCompletionTime);
- }
-
- return new FeedMedia(
- mediaId,
- null,
- cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX),
- cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX),
- cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX),
- cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX),
- cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX),
- cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX),
- cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0,
- playbackCompletionDate,
- cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX));
- }
-
- private static Feed extractFeedFromCursorRow(PodDBAdapter adapter,
- Cursor cursor) {
- Date lastUpdate = new Date(
- cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_LASTUPDATE));
-
- final FeedImage image;
- long imageIndex = cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_IMAGE);
- if (imageIndex != 0) {
- image = getFeedImage(adapter, imageIndex);
- } else {
- image = null;
- }
- Feed feed = new Feed(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID),
- lastUpdate,
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TITLE),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LINK),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DESCRIPTION),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_PAYMENT_LINK),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_AUTHOR),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LANGUAGE),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TYPE),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FEED_IDENTIFIER),
- image,
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL),
- cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0,
- new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS)));
-
- if (image != null) {
- image.setOwner(feed);
- }
-
- FeedPreferences preferences = new FeedPreferences(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID),
- cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0,
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_USERNAME),
- cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_PASSWORD));
-
- feed.setPreferences(preferences);
- return feed;
- }
-
- private static FeedItem getMatchingItemForMedia(long itemId,
- List<FeedItem> items) {
- for (FeedItem item : items) {
- if (item.getId() == itemId) {
- return item;
- }
- }
- return null;
- }
-
- static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting queue");
-
- Cursor itemlistCursor = adapter.getQueueCursor();
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
- itemlistCursor.close();
- loadFeedDataOfFeedItemlist(context, items);
-
- return items;
- }
-
- /**
- * Loads the IDs of the FeedItems in the queue. This method should be preferred over
- * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned
- * list in a {@link de.danoeh.antennapod.util.QueueAccess} object for easier access to the queue's properties.
- */
- public static List<Long> getQueueIDList(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
-
- adapter.open();
- List<Long> result = getQueueIDList(adapter);
- adapter.close();
-
- return result;
- }
-
- static List<Long> getQueueIDList(PodDBAdapter adapter) {
- adapter.open();
- Cursor queueCursor = adapter.getQueueIDCursor();
-
- List<Long> queueIds = new ArrayList<Long>(queueCursor.getCount());
- if (queueCursor.moveToFirst()) {
- do {
- queueIds.add(queueCursor.getLong(0));
- } while (queueCursor.moveToNext());
- }
- return queueIds;
- }
-
-
- /**
- * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using
- * {@link #getQueueIDList(android.content.Context)} instead.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned
- * list in a {@link de.danoeh.antennapod.util.QueueAccess} object for easier access to the queue's properties.
- */
- public static List<FeedItem> getQueue(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting queue");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- List<FeedItem> items = getQueue(context, adapter);
- adapter.close();
- return items;
- }
-
- /**
- * Loads a list of FeedItems whose episode has been downloaded.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of FeedItems whose episdoe has been downloaded.
- */
- public static List<FeedItem> getDownloadedItems(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting downloaded items");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
-
- Cursor itemlistCursor = adapter.getDownloadedItemsCursor();
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
- itemlistCursor.close();
- loadFeedDataOfFeedItemlist(context, items);
- Collections.sort(items, new FeedItemPubdateComparator());
-
- adapter.close();
- return items;
-
- }
-
- /**
- * Loads a list of FeedItems whose 'read'-attribute is set to false.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used,
- * consider using {@link #getUnreadItemIds(android.content.Context)} instead.
- */
- public static List<FeedItem> getUnreadItemsList(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting unread items list");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
-
- Cursor itemlistCursor = adapter.getUnreadItemsCursor();
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
- itemlistCursor.close();
-
- loadFeedDataOfFeedItemlist(context, items);
-
- adapter.close();
-
- return items;
- }
-
- /**
- * Loads the IDs of the FeedItems whose 'read'-attribute is set to false.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred
- * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used.
- */
- public static long[] getUnreadItemIds(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor cursor = adapter.getUnreadItemIdsCursor();
- long[] itemIds = new long[cursor.getCount()];
- int i = 0;
- if (cursor.moveToFirst()) {
- do {
- itemIds[i] = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- i++;
- } while (cursor.moveToNext());
- }
- return itemIds;
- }
-
-
- /**
- * Loads a list of FeedItems sorted by pubDate in descending order.
- *
- * @param context A context that is used for opening a database connection.
- * @param limit The maximum number of episodes that should be loaded.
- */
- public static List<FeedItem> getRecentlyPublishedEpisodes(Context context, int limit) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting recently published items list");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
-
- Cursor itemlistCursor = adapter.getRecentlyPublishedItemsCursor(limit);
- List<FeedItem> items = extractItemlistFromCursor(adapter,
- itemlistCursor);
- itemlistCursor.close();
-
- loadFeedDataOfFeedItemlist(context, items);
-
- adapter.close();
-
- return items;
- }
-
- /**
- * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode
- * has been completed at least once.
- *
- * @param context A context that is used for opening a database connection.
- * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order.
- * The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}.
- */
- public static List<FeedItem> getPlaybackHistory(final Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading playback history");
- final int PLAYBACK_HISTORY_SIZE = 50;
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
-
- Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE);
- String[] itemIds = new String[mediaCursor.getCount()];
- for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) {
- itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX));
- }
- mediaCursor.close();
- Cursor itemCursor = adapter.getFeedItemCursor(itemIds);
- List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor);
- loadFeedDataOfFeedItemlist(context, items);
- itemCursor.close();
- adapter.close();
-
- Collections.sort(items, new PlaybackCompletionDateComparator());
- return items;
- }
-
- /**
- * Loads the download log from the database.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list with DownloadStatus objects that represent the download log.
- * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}.
- */
- public static List<DownloadStatus> getDownloadLog(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Extracting DownloadLog");
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE);
- List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>(
- logCursor.getCount());
-
- if (logCursor.moveToFirst()) {
- do {
- long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
-
- long feedfileId = logCursor
- .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX);
- int feedfileType = logCursor
- .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX);
- boolean successful = logCursor
- .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0;
- int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX);
- String reasonDetailed = logCursor
- .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX);
- String title = logCursor
- .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX);
- Date completionDate = new Date(
- logCursor
- .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)
- );
- downloadLog.add(new DownloadStatus(id, title, feedfileId,
- feedfileType, successful, DownloadError.fromCode(reason), completionDate,
- reasonDetailed));
-
- } while (logCursor.moveToNext());
- }
- logCursor.close();
- Collections.sort(downloadLog, new DownloadStatusComparator());
- return downloadLog;
- }
-
- /**
- * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over
- * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)} if only metadata about
- * the FeedItems is needed.
- *
- * @param context A context that is used for opening a database connection.
- * @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title.
- */
- public static List<FeedItemStatistics> getFeedStatisticsList(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- List<FeedItemStatistics> result = new ArrayList<FeedItemStatistics>();
- Cursor cursor = adapter.getFeedStatisticsCursor();
- if (cursor.moveToFirst()) {
- do {
- result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED),
- cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS),
- cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS),
- cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES),
- new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE))));
- } while (cursor.moveToNext());
- }
-
- cursor.close();
- adapter.close();
- return result;
- }
-
- /**
- * Loads a specific Feed from the database.
- *
- * @param context A context that is used for opening a database connection.
- * @param feedId The ID of the Feed
- * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
- * database and the items-attribute will be set correctly.
- */
- public static Feed getFeed(final Context context, final long feedId) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Feed result = getFeed(context, feedId, adapter);
- adapter.close();
- return result;
- }
-
- static Feed getFeed(final Context context, final long feedId, PodDBAdapter adapter) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading feed with id " + feedId);
- Feed feed = null;
-
- Cursor feedCursor = adapter.getFeedCursor(feedId);
- if (feedCursor.moveToFirst()) {
- feed = extractFeedFromCursorRow(adapter, feedCursor);
- feed.setItems(getFeedItemList(context, feed));
- } else {
- Log.e(TAG, "getFeed could not find feed with id " + feedId);
- }
- feedCursor.close();
- return feed;
- }
-
- static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading feeditem with id " + itemId);
- FeedItem item = null;
-
- Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId));
- if (itemCursor.moveToFirst()) {
- List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor);
- if (list.size() > 0) {
- item = list.get(0);
- loadFeedDataOfFeedItemlist(context, list);
- }
- }
- return item;
-
- }
-
- /**
- * Loads a specific FeedItem from the database.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId The ID of the FeedItem
- * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes of the FeedItem will
- * also be loaded from the database.
- */
- public static FeedItem getFeedItem(final Context context, final long itemId) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Loading feeditem with id " + itemId);
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- FeedItem item = getFeedItem(context, itemId, adapter);
- adapter.close();
- return item;
-
- }
-
- /**
- * Loads additional information about a FeedItem, e.g. shownotes
- *
- * @param context A context that is used for opening a database connection.
- * @param item The FeedItem
- */
- public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor extraCursor = adapter.getExtraInformationOfItem(item);
- if (extraCursor.moveToFirst()) {
- String description = extraCursor
- .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION);
- String contentEncoded = extraCursor
- .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED);
- item.setDescription(description);
- item.setContentEncoded(contentEncoded);
- }
- adapter.close();
- }
-
- /**
- * Returns the number of downloaded episodes.
- *
- * @param context A context that is used for opening a database connection.
- * @return The number of downloaded episodes.
- */
- public static int getNumberOfDownloadedEpisodes(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final int result = adapter.getNumberOfDownloadedEpisodes();
- adapter.close();
- return result;
- }
-
- /**
- * Returns the number of unread items.
- *
- * @param context A context that is used for opening a database connection.
- * @return The number of unread items.
- */
- public static int getNumberOfUnreadItems(final Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final int result = adapter.getNumberOfUnreadItems();
- adapter.close();
- return result;
- }
-
- /**
- * Searches the DB for a FeedImage of the given id.
- *
- * @param context A context that is used for opening a database connection.
- * @param imageId The id of the object
- * @return The found object
- */
- public static FeedImage getFeedImage(final Context context, final long imageId) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- FeedImage result = getFeedImage(adapter, imageId);
- adapter.close();
- return result;
- }
-
- /**
- * Searches the DB for a FeedImage of the given id.
- *
- * @param id The id of the object
- * @return The found object
- */
- static FeedImage getFeedImage(PodDBAdapter adapter, final long id) {
- Cursor cursor = adapter.getImageCursor(id);
- if ((cursor.getCount() == 0) || !cursor.moveToFirst()) {
- throw new SQLException("No FeedImage found at index: " + id);
- }
- FeedImage image = new FeedImage(id, cursor.getString(cursor
- .getColumnIndex(PodDBAdapter.KEY_TITLE)),
- cursor.getString(cursor
- .getColumnIndex(PodDBAdapter.KEY_FILE_URL)),
- cursor.getString(cursor
- .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)),
- cursor.getInt(cursor
- .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0
- );
- cursor.close();
- return image;
- }
-
- /**
- * Searches the DB for a FeedMedia of the given id.
- *
- * @param context A context that is used for opening a database connection.
- * @param mediaId The id of the object
- * @return The found object
- */
- public static FeedMedia getFeedMedia(final Context context, final long mediaId) {
- PodDBAdapter adapter = new PodDBAdapter(context);
-
- adapter.open();
- Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId);
-
- FeedMedia media = null;
- if (mediaCursor.moveToFirst()) {
- final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX);
- media = extractFeedMediaFromCursorRow(mediaCursor);
- FeedItem item = getFeedItem(context, itemId);
- if (media != null && item != null) {
- media.setItem(item);
- item.setMedia(media);
- }
- }
-
- mediaCursor.close();
- adapter.close();
-
- return media;
- }
-
- /**
- * Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems.
- *
- * @param context A context that is used for opening a database connection.
- * @return The flattr queue as a List.
- */
- public static List<FlattrThing> getFlattrQueue(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- List<FlattrThing> result = new ArrayList<FlattrThing>();
-
- // load feeds
- Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor();
- if (feedCursor.moveToFirst()) {
- do {
- result.add(extractFeedFromCursorRow(adapter, feedCursor));
- } while (feedCursor.moveToNext());
- }
- feedCursor.close();
-
- //load feed items
- Cursor feedItemCursor = adapter.getFeedItemsInFlattrQueueCursor();
- result.addAll(extractItemlistFromCursor(adapter, feedItemCursor));
- feedItemCursor.close();
-
- adapter.close();
- Log.d(TAG, "Returning flattrQueueIterator for queue with " + result.size() + " items.");
- return result;
- }
-
-
- /**
- * Returns true if the flattr queue is empty.
- *
- * @param context A context that is used for opening a database connection.
- */
- public static boolean getFlattrQueueEmpty(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- boolean empty = adapter.getFlattrQueueSize() == 0;
- adapter.close();
- return empty;
- }
-
- /**
- * Returns data necessary for displaying the navigation drawer. This includes
- * the list of subscriptions, the number of items in the queue and the number of unread
- * items.
- *
- * @param context A context that is used for opening a database connection.
- */
- public static NavDrawerData getNavDrawerData(Context context) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- List<Feed> feeds = getFeedList(adapter);
- int queueSize = adapter.getQueueSize();
- int numUnreadItems = adapter.getNumberOfUnreadItems();
- NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems);
- adapter.close();
- return result;
- }
-
- public static class NavDrawerData {
- public List<Feed> feeds;
- public int queueSize;
- public int numUnreadItems;
-
- public NavDrawerData(List<Feed> feeds, int queueSize, int numUnreadItems) {
- this.feeds = feeds;
- this.queueSize = queueSize;
- this.numUnreadItems = numUnreadItems;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java
deleted file mode 100644
index a230ba797..000000000
--- a/src/de/danoeh/antennapod/storage/DBTasks.java
+++ /dev/null
@@ -1,895 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.asynctask.FlattrClickWorker;
-import de.danoeh.antennapod.asynctask.FlattrStatusFetcher;
-import de.danoeh.antennapod.feed.*;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.GpodnetSyncService;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.util.DownloadError;
-import de.danoeh.antennapod.util.NetworkUtils;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator;
-import de.danoeh.antennapod.util.exception.MediaFileNotFoundException;
-import de.danoeh.antennapod.util.flattr.FlattrUtils;
-
-import java.util.*;
-import java.util.concurrent.*;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Provides methods for doing common tasks that use DBReader and DBWriter.
- */
-public final class DBTasks {
- private static final String TAG = "DBTasks";
-
- /**
- * Executor service used by the autodownloadUndownloadedEpisodes method.
- */
- private static ExecutorService autodownloadExec;
-
- static {
- autodownloadExec = Executors.newSingleThreadExecutor(new ThreadFactory() {
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- });
- }
-
- private DBTasks() {
- }
-
- /**
- * Removes the feed with the given download url. This method should NOT be executed on the GUI thread.
- *
- * @param context Used for accessing the db
- * @param downloadUrl URL of the feed.
- */
- public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor cursor = adapter.getFeedCursorDownloadUrls();
- long feedID = 0;
- if (cursor.moveToFirst()) {
- do {
- if (cursor.getString(1).equals(downloadUrl)) {
- feedID = cursor.getLong(0);
- }
- } while (cursor.moveToNext());
- }
- cursor.close();
- adapter.close();
-
- if (feedID != 0) {
- try {
- DBWriter.deleteFeed(context, feedID).get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- } else {
- Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl);
- }
- }
-
- /**
- * Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to
- * start the {@link PlaybackService}.
- *
- * @param context Used for sending starting Services and Activities.
- * @param media The FeedMedia object.
- * @param showPlayer If true, starts the appropriate player activity ({@link de.danoeh.antennapod.activity.AudioplayerActivity}
- * or {@link de.danoeh.antennapod.activity.VideoplayerActivity}
- * @param startWhenPrepared Parameter for the {@link PlaybackService} start intent. If true, playback will start as
- * soon as the PlaybackService has finished loading the FeedMedia object's file.
- * @param shouldStream Parameter for the {@link PlaybackService} start intent. If true, the FeedMedia object's file
- * will be streamed, otherwise the downloaded file will be used. If the downloaded file cannot be
- * found, the PlaybackService will shutdown and the database entry of the FeedMedia object will be
- * corrected.
- */
- public static void playMedia(final Context context, final FeedMedia media,
- boolean showPlayer, boolean startWhenPrepared, boolean shouldStream) {
- try {
- if (!shouldStream) {
- if (media.fileExists() == false) {
- throw new MediaFileNotFoundException(
- "No episode was found at " + media.getFile_url(),
- media);
- }
- }
- // Start playback Service
- Intent launchIntent = new Intent(context, PlaybackService.class);
- launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
- launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
- startWhenPrepared);
- launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
- shouldStream);
- launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
- true);
- context.startService(launchIntent);
- if (showPlayer) {
- // Launch media player
- context.startActivity(PlaybackService.getPlayerActivityIntent(
- context, media));
- }
- DBWriter.addQueueItemAt(context, media.getItem().getId(), 0, false);
- } catch (MediaFileNotFoundException e) {
- e.printStackTrace();
- if (media.isPlaying()) {
- context.sendBroadcast(new Intent(
- PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- notifyMissingFeedMediaFile(context, media);
- }
- }
-
- private static AtomicBoolean isRefreshing = new AtomicBoolean(false);
-
- /**
- * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still
- * enqueuing Feeds for download from a previous call
- *
- * @param context Might be used for accessing the database
- * @param feeds List of Feeds that should be refreshed.
- */
- public static void refreshAllFeeds(final Context context,
- final List<Feed> feeds) {
- if (isRefreshing.compareAndSet(false, true)) {
- new Thread() {
- public void run() {
- if (feeds != null) {
- refreshFeeds(context, feeds);
- } else {
- refreshFeeds(context, DBReader.getFeedList(context));
- }
- isRefreshing.set(false);
-
- if (FlattrUtils.hasToken()) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Flattring all pending things.");
- new FlattrClickWorker(context).executeAsync(); // flattr pending things
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Fetching flattr status.");
- new FlattrStatusFetcher(context).start();
-
- }
- GpodnetSyncService.sendSyncIntent(context);
- autodownloadUndownloadedItems(context);
- }
- }.start();
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Ignoring request to refresh all feeds: Refresh lock is locked");
- }
- }
-
- /**
- * Used by refreshExpiredFeeds to determine which feeds should be refreshed.
- * This method will use the value specified in the UserPreferences as the
- * expiration time.
- *
- * @param context Used for DB access.
- * @return A list of expired feeds. An empty list will be returned if there
- * are no expired feeds.
- */
- public static List<Feed> getExpiredFeeds(final Context context) {
- long millis = UserPreferences.getUpdateInterval();
-
- if (millis > 0) {
-
- List<Feed> feedList = DBReader.getExpiredFeedsList(context,
- millis);
- if (feedList.size() > 0) {
- refreshFeeds(context, feedList);
- }
- return feedList;
- } else {
- return new ArrayList<Feed>();
- }
- }
-
- /**
- * Refreshes expired Feeds in the list returned by the getExpiredFeedsList(Context, long) method in DBReader.
- * The expiration date parameter is determined by the update interval specified in {@link UserPreferences}.
- *
- * @param context Used for DB access.
- */
- public static void refreshExpiredFeeds(final Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Refreshing expired feeds");
-
- new Thread() {
- public void run() {
- refreshFeeds(context, getExpiredFeeds(context));
- }
- }.start();
- }
-
- private static void refreshFeeds(final Context context,
- final List<Feed> feedList) {
-
- for (Feed feed : feedList) {
- try {
- refreshFeed(context, feed);
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- DBWriter.addDownloadStatus(
- context,
- new DownloadStatus(feed, feed
- .getHumanReadableIdentifier(),
- DownloadError.ERROR_REQUEST_ERROR, false, e
- .getMessage()
- )
- );
- }
- }
-
- }
-
- /**
- * Updates a specific Feed.
- *
- * @param context Used for requesting the download.
- * @param feed The Feed object.
- */
- public static void refreshFeed(Context context, Feed feed)
- throws DownloadRequestException {
- Feed f;
- if (feed.getPreferences() == null) {
- f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle());
- } else {
- f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle(),
- feed.getPreferences().getUsername(), feed.getPreferences().getPassword());
- }
- f.setId(feed.getId());
- DownloadRequester.getInstance().downloadFeed(context, f);
- }
-
- /**
- * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file.
- *
- * @param context Used for requesting the download.
- * @param image The FeedImage object.
- */
- public static void notifyInvalidImageFile(final Context context,
- final FeedImage image) {
- Log.i(TAG,
- "The DB was notified about an invalid image download. It will now try to re-download the image file");
- try {
- DownloadRequester.getInstance().downloadImage(context, image);
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- Log.w(TAG, "Failed to download invalid feed image");
- }
- }
-
- /**
- * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the
- * DB and send a FeedUpdateBroadcast.
- */
- public static void notifyMissingFeedMediaFile(final Context context,
- final FeedMedia media) {
- Log.i(TAG,
- "The feedmanager was notified about a missing episode. It will update its database now.");
- media.setDownloaded(false);
- media.setFile_url(null);
- DBWriter.setFeedMedia(context, media);
- EventDistributor.getInstance().sendFeedUpdateBroadcast();
- }
-
- /**
- * Request the download of all objects in the queue. from a separate Thread.
- *
- * @param context Used for requesting the download an accessing the database.
- */
- public static void downloadAllItemsInQueue(final Context context) {
- new Thread() {
- public void run() {
- List<FeedItem> queue = DBReader.getQueue(context);
- if (!queue.isEmpty()) {
- try {
- downloadFeedItems(context,
- queue.toArray(new FeedItem[queue.size()]));
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- }
- }
- }
- }.start();
- }
-
- /**
- * Requests the download of a list of FeedItem objects.
- *
- * @param context Used for requesting the download and accessing the DB.
- * @param items The FeedItem objects.
- */
- public static void downloadFeedItems(final Context context,
- FeedItem... items) throws DownloadRequestException {
- downloadFeedItems(true, context, items);
- }
-
- private static void downloadFeedItems(boolean performAutoCleanup,
- final Context context, final FeedItem... items)
- throws DownloadRequestException {
- final DownloadRequester requester = DownloadRequester.getInstance();
-
- if (performAutoCleanup) {
- new Thread() {
-
- @Override
- public void run() {
- performAutoCleanup(context,
- getPerformAutoCleanupArgs(context, items.length));
- }
-
- }.start();
- }
- for (FeedItem item : items) {
- if (item.getMedia() != null
- && !requester.isDownloadingFile(item.getMedia())
- && !item.getMedia().isDownloaded()) {
- if (items.length > 1) {
- try {
- requester.downloadMedia(context, item.getMedia());
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- DBWriter.addDownloadStatus(context,
- new DownloadStatus(item.getMedia(), item
- .getMedia()
- .getHumanReadableIdentifier(),
- DownloadError.ERROR_REQUEST_ERROR,
- false, e.getMessage()
- )
- );
- }
- } else {
- requester.downloadMedia(context, item.getMedia());
- }
- }
- }
- }
-
- private static int getNumberOfUndownloadedEpisodes(
- final List<FeedItem> queue, final List<FeedItem> unreadItems) {
- int counter = 0;
- for (FeedItem item : queue) {
- if (item.hasMedia() && !item.getMedia().isDownloaded()
- && !item.getMedia().isPlaying()
- && item.getFeed().getPreferences().getAutoDownload()) {
- counter++;
- }
- }
- for (FeedItem item : unreadItems) {
- if (item.hasMedia() && !item.getMedia().isDownloaded()
- && item.getFeed().getPreferences().getAutoDownload()) {
- counter++;
- }
- }
- return counter;
- }
-
- /**
- * Looks for undownloaded episodes in the queue or list of unread items and request a download if
- * 1. Network is available
- * 2. There is free space in the episode cache
- * This method is executed on an internal single thread executor.
- *
- * @param context Used for accessing the DB.
- * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if
- * its media ID is in the mediaIds list.
- * @return A Future that can be used for waiting for the methods completion.
- */
- public static Future<?> autodownloadUndownloadedItems(final Context context, final long... mediaIds) {
- return autodownloadExec.submit(new Runnable() {
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Performing auto-dl of undownloaded episodes");
- if (NetworkUtils.autodownloadNetworkAvailable(context)
- && UserPreferences.isEnableAutodownload()) {
- final List<FeedItem> queue = DBReader.getQueue(context);
- final List<FeedItem> unreadItems = DBReader
- .getUnreadItemsList(context);
-
- int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue,
- unreadItems);
- int downloadedEpisodes = DBReader
- .getNumberOfDownloadedEpisodes(context);
- int deletedEpisodes = performAutoCleanup(context,
- getPerformAutoCleanupArgs(context, undownloadedEpisodes));
- int episodeSpaceLeft = undownloadedEpisodes;
- boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences
- .getEpisodeCacheSizeUnlimited();
-
- if (!cacheIsUnlimited
- && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes
- + undownloadedEpisodes) {
- episodeSpaceLeft = UserPreferences.getEpisodeCacheSize()
- - (downloadedEpisodes - deletedEpisodes);
- }
-
- Arrays.sort(mediaIds); // sort for binary search
- final boolean ignoreMediaIds = mediaIds.length == 0;
- List<FeedItem> itemsToDownload = new ArrayList<FeedItem>();
-
- if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
- for (int i = 0; i < queue.size(); i++) { // ignore playing item
- FeedItem item = queue.get(i);
- long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
- if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
- && item.hasMedia()
- && !item.getMedia().isDownloaded()
- && !item.getMedia().isPlaying()
- && item.getFeed().getPreferences().getAutoDownload()) {
- itemsToDownload.add(item);
- episodeSpaceLeft--;
- undownloadedEpisodes--;
- if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
- break;
- }
- }
- }
- }
-
- if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) {
- for (FeedItem item : unreadItems) {
- long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1;
- if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0)
- && item.hasMedia()
- && !item.getMedia().isDownloaded()
- && item.getFeed().getPreferences().getAutoDownload()) {
- itemsToDownload.add(item);
- episodeSpaceLeft--;
- undownloadedEpisodes--;
- if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) {
- break;
- }
- }
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Enqueueing " + itemsToDownload.size()
- + " items for download");
-
- try {
- downloadFeedItems(false, context,
- itemsToDownload.toArray(new FeedItem[itemsToDownload
- .size()])
- );
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- }
-
- }
- }
- });
-
- }
-
- private static int getPerformAutoCleanupArgs(Context context,
- final int episodeNumber) {
- if (episodeNumber >= 0
- && UserPreferences.getEpisodeCacheSize() != UserPreferences
- .getEpisodeCacheSizeUnlimited()) {
- int downloadedEpisodes = DBReader
- .getNumberOfDownloadedEpisodes(context);
- if (downloadedEpisodes + episodeNumber >= UserPreferences
- .getEpisodeCacheSize()) {
-
- return downloadedEpisodes + episodeNumber
- - UserPreferences.getEpisodeCacheSize();
- }
- }
- return 0;
- }
-
- /**
- * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller
- * 'playbackCompletionDate'-value will be deleted first.
- * <p/>
- * This method should NOT be executed on the GUI thread.
- *
- * @param context Used for accessing the DB.
- */
- public static void performAutoCleanup(final Context context) {
- performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0));
- }
-
- private static int performAutoCleanup(final Context context,
- final int episodeNumber) {
- List<FeedItem> candidates = new ArrayList<FeedItem>();
- List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context);
- QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context));
- List<FeedItem> delete;
- for (FeedItem item : downloadedItems) {
- if (item.hasMedia() && item.getMedia().isDownloaded()
- && !queue.contains(item.getId()) && item.isRead()) {
- candidates.add(item);
- }
-
- }
-
- Collections.sort(candidates, new Comparator<FeedItem>() {
- @Override
- public int compare(FeedItem lhs, FeedItem rhs) {
- Date l = lhs.getMedia().getPlaybackCompletionDate();
- Date r = rhs.getMedia().getPlaybackCompletionDate();
-
- if (l == null) {
- l = new Date(0);
- }
- if (r == null) {
- r = new Date(0);
- }
- return l.compareTo(r);
- }
- });
-
- if (candidates.size() > episodeNumber) {
- delete = candidates.subList(0, episodeNumber);
- } else {
- delete = candidates;
- }
-
- for (FeedItem item : delete) {
- try {
- DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- }
-
- int counter = delete.size();
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, String.format(
- "Auto-delete deleted %d episodes (%d requested)", counter,
- episodeNumber));
-
- return counter;
- }
-
- /**
- * Adds all FeedItem objects whose 'read'-attribute is false to the queue in a separate thread.
- */
- public static void enqueueAllNewItems(final Context context) {
- long[] unreadItems = DBReader.getUnreadItemIds(context);
- DBWriter.addQueueItem(context, unreadItems);
- }
-
- /**
- * Returns the successor of a FeedItem in the queue.
- *
- * @param context Used for accessing the DB.
- * @param itemId ID of the FeedItem
- * @param queue Used for determining the successor of the item. If this parameter is null, the method will load
- * the queue from the database in the same thread.
- * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor.
- */
- public static FeedItem getQueueSuccessorOfItem(Context context,
- final long itemId, List<FeedItem> queue) {
- FeedItem result = null;
- if (queue == null) {
- queue = DBReader.getQueue(context);
- }
- if (queue != null) {
- Iterator<FeedItem> iterator = queue.iterator();
- while (iterator.hasNext()) {
- FeedItem item = iterator.next();
- if (item.getId() == itemId) {
- if (iterator.hasNext()) {
- result = iterator.next();
- }
- break;
- }
- }
- }
- return result;
- }
-
- /**
- * Loads the queue from the database and checks if the specified FeedItem is in the queue.
- * This method should NOT be executed in the GUI thread.
- *
- * @param context Used for accessing the DB.
- * @param feedItemId ID of the FeedItem
- */
- public static boolean isInQueue(Context context, final long feedItemId) {
- List<Long> queue = DBReader.getQueueIDList(context);
- return QueueAccess.IDListAccess(queue).contains(feedItemId);
- }
-
- private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter,
- Feed feed) {
- if (feed.getId() != 0) {
- return DBReader.getFeed(context, feed.getId(), adapter);
- } else {
- List<Feed> feeds = DBReader.getFeedList(context);
- for (Feed f : feeds) {
- if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) {
- f.setItems(DBReader.getFeedItemList(context, f));
- return f;
- }
- }
- }
- return null;
- }
-
- /**
- * Get a FeedItem by its identifying value.
- */
- private static FeedItem searchFeedItemByIdentifyingValue(Feed feed,
- String identifier) {
- for (FeedItem item : feed.getItems()) {
- if (item.getIdentifyingValue().equals(identifier)) {
- return item;
- }
- }
- return null;
- }
-
- /**
- * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same
- * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed.
- * These FeedItems will be marked as unread.
- * <p/>
- * This method can update multiple feeds at once. Submitting a feed twice in the same method call can result in undefined behavior.
- * <p/>
- * This method should NOT be executed on the GUI thread.
- *
- * @param context Used for accessing the DB.
- * @param newFeeds The new Feed objects.
- * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise.
- */
- public static synchronized Feed[] updateFeed(final Context context,
- final Feed... newFeeds) {
- List<Feed> newFeedsList = new ArrayList<Feed>();
- List<Feed> updatedFeedsList = new ArrayList<Feed>();
- Feed[] resultFeeds = new Feed[newFeeds.length];
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
-
- for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) {
-
- final Feed newFeed = newFeeds[feedIdx];
-
- // Look up feed in the feedslist
- final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter,
- newFeed);
- if (savedFeed == null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Found no existing Feed with title "
- + newFeed.getTitle() + ". Adding as new one."
- );
- // Add a new Feed
- newFeedsList.add(newFeed);
- resultFeeds[feedIdx] = newFeed;
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Feed with title " + newFeed.getTitle()
- + " already exists. Syncing new with existing one.");
-
- Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
- if (savedFeed.compareWithOther(newFeed)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Feed has updated attribute values. Updating old feed's attributes");
- savedFeed.updateFromOther(newFeed);
- }
- if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences");
- savedFeed.getPreferences().updateFromOther(newFeed.getPreferences());
- }
- // Look for new or updated Items
- for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
- final FeedItem item = newFeed.getItems().get(idx);
- FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed,
- item.getIdentifyingValue());
- if (oldItem == null) {
- // item is new
- final int i = idx;
- item.setFeed(savedFeed);
- savedFeed.getItems().add(i, item);
- item.setRead(false);
- } else {
- oldItem.updateFromOther(item);
- }
- }
- // update attributes
- savedFeed.setLastUpdate(newFeed.getLastUpdate());
- savedFeed.setType(newFeed.getType());
-
- updatedFeedsList.add(savedFeed);
- resultFeeds[feedIdx] = savedFeed;
- }
- }
-
- adapter.close();
-
- try {
- DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get();
- DBWriter.setCompleteFeed(context, updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
-
- EventDistributor.getInstance().sendFeedUpdateBroadcast();
-
- return resultFeeds;
- }
-
- /**
- * Searches the titles of FeedItems of a specific Feed for a given
- * string.
- *
- * @param context Used for accessing the DB.
- * @param feedID The id of the feed whose items should be searched.
- * @param query The search string.
- * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
- */
- public static FutureTask<List<FeedItem>> searchFeedItemTitle(final Context context,
- final long feedID, final String query) {
- return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
- @Override
- public void execute(PodDBAdapter adapter) {
- Cursor searchResult = adapter.searchItemTitles(feedID,
- query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
- setResult(items);
- searchResult.close();
- }
- });
- }
-
- /**
- * Searches the descriptions of FeedItems of a specific Feed for a given
- * string.
- *
- * @param context Used for accessing the DB.
- * @param feedID The id of the feed whose items should be searched.
- * @param query The search string
- * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
- */
- public static FutureTask<List<FeedItem>> searchFeedItemDescription(final Context context,
- final long feedID, final String query) {
- return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
- @Override
- public void execute(PodDBAdapter adapter) {
- Cursor searchResult = adapter.searchItemDescriptions(feedID,
- query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
- setResult(items);
- searchResult.close();
- }
- });
- }
-
- /**
- * Searches the contentEncoded-value of FeedItems of a specific Feed for a given
- * string.
- *
- * @param context Used for accessing the DB.
- * @param feedID The id of the feed whose items should be searched.
- * @param query The search string
- * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
- */
- public static FutureTask<List<FeedItem>> searchFeedItemContentEncoded(final Context context,
- final long feedID, final String query) {
- return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
- @Override
- public void execute(PodDBAdapter adapter) {
- Cursor searchResult = adapter.searchItemContentEncoded(feedID,
- query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
- setResult(items);
- searchResult.close();
- }
- });
- }
-
- /**
- * Searches chapters of the FeedItems of a specific Feed for a given string.
- *
- * @param context Used for accessing the DB.
- * @param feedID The id of the feed whose items should be searched.
- * @param query The search string
- * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
- */
- public static FutureTask<List<FeedItem>> searchFeedItemChapters(final Context context,
- final long feedID, final String query) {
- return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) {
- @Override
- public void execute(PodDBAdapter adapter) {
- Cursor searchResult = adapter.searchItemChapters(feedID,
- query);
- List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult);
- DBReader.loadFeedDataOfFeedItemlist(context, items);
- setResult(items);
- searchResult.close();
- }
- });
- }
-
- /**
- * A runnable which should be used for database queries. The onCompletion
- * method is executed on the database executor to handle Cursors correctly.
- * This class automatically creates a PodDBAdapter object and closes it when
- * it is no longer in use.
- */
- static abstract class QueryTask<T> implements Callable<T> {
- private T result;
- private Context context;
-
- public QueryTask(Context context) {
- this.context = context;
- }
-
- @Override
- public T call() throws Exception {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- execute(adapter);
- adapter.close();
- return result;
- }
-
- public abstract void execute(PodDBAdapter adapter);
-
- protected void setResult(T result) {
- this.result = result;
- }
- }
-
- /**
- * Adds the given FeedItem to the flattr queue if the user is logged in. Otherwise, a dialog
- * will be opened that lets the user go either to the login screen or the website of the flattr thing.
- *
- * @param context
- * @param item
- */
- public static void flattrItemIfLoggedIn(Context context, FeedItem item) {
- if (FlattrUtils.hasToken()) {
- item.getFlattrStatus().setFlattrQueue();
- DBWriter.setFlattredStatus(context, item, true);
- } else {
- FlattrUtils.showNoTokenDialogOrRedirect(context, item.getPaymentLink());
- }
- }
-
- /**
- * Adds the given Feed to the flattr queue if the user is logged in. Otherwise, a dialog
- * will be opened that lets the user go either to the login screen or the website of the flattr thing.
- *
- * @param context
- * @param feed
- */
- public static void flattrFeedIfLoggedIn(Context context, Feed feed) {
- if (FlattrUtils.hasToken()) {
- feed.getFlattrStatus().setFlattrQueue();
- DBWriter.setFlattredStatus(context, feed, true);
- } else {
- FlattrUtils.showNoTokenDialogOrRedirect(context, feed.getPaymentLink());
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java
deleted file mode 100644
index 9916ac97f..000000000
--- a/src/de/danoeh/antennapod/storage/DBWriter.java
+++ /dev/null
@@ -1,974 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-import android.app.backup.BackupManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.preference.PreferenceManager;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.asynctask.FlattrClickWorker;
-import de.danoeh.antennapod.feed.*;
-import de.danoeh.antennapod.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-import de.danoeh.antennapod.util.flattr.FlattrThing;
-import de.danoeh.antennapod.util.flattr.SimpleFlattrThing;
-import org.shredzone.flattr4j.model.Flattr;
-
-import java.io.File;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Date;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ThreadFactory;
-
-/**
- * Provides methods for writing data to AntennaPod's database.
- * In general, DBWriter-methods will be executed on an internal ExecutorService.
- * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's
- * will NOT contain any results.
- * The caller can also use the {@link EventDistributor} in order to be notified about the method's completion asynchronously.
- * This class will use the {@link EventDistributor} to notify listeners about changes in the database.
- */
-public class DBWriter {
- private static final String TAG = "DBWriter";
-
- private static final ExecutorService dbExec;
-
- static {
- dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- });
- }
-
- private DBWriter() {
- }
-
- /**
- * Deletes a downloaded FeedMedia file from the storage device.
- *
- * @param context A context that is used for opening a database connection.
- * @param mediaId ID of the FeedMedia object whose downloaded file should be deleted.
- */
- public static Future<?> deleteFeedMediaOfItem(final Context context,
- final long mediaId) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
-
- final FeedMedia media = DBReader.getFeedMedia(context, mediaId);
- if (media != null) {
- Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s",
- media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded())));
- boolean result = false;
- if (media.isDownloaded()) {
- // delete downloaded media file
- File mediaFile = new File(media.getFile_url());
- if (mediaFile.exists()) {
- result = mediaFile.delete();
- }
- media.setDownloaded(false);
- media.setFile_url(null);
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setMedia(media);
- adapter.close();
-
- // If media is currently being played, change playback
- // type to 'stream' and shutdown playback service
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(context);
- if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) {
- if (media.getId() == PlaybackPreferences
- .getCurrentlyPlayingFeedMediaId()) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(
- PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
- true);
- editor.commit();
- }
- if (PlaybackPreferences
- .getCurrentlyPlayingFeedMediaId() == media
- .getId()) {
- context.sendBroadcast(new Intent(
- PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- }
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Deleting File. Result: " + result);
- EventDistributor.getInstance().sendQueueUpdateBroadcast();
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
- }
- }
- });
- }
-
- /**
- * Deletes a Feed and all downloaded files of its components like images and downloaded episodes.
- *
- * @param context A context that is used for opening a database connection.
- * @param feedId ID of the Feed that should be deleted.
- */
- public static Future<?> deleteFeed(final Context context, final long feedId) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- DownloadRequester requester = DownloadRequester.getInstance();
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(context
- .getApplicationContext());
- final Feed feed = DBReader.getFeed(context, feedId);
- if (feed != null) {
- if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA
- && PlaybackPreferences.getLastPlayedFeedId() == feed
- .getId()) {
- context.sendBroadcast(new Intent(
- PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
- SharedPreferences.Editor editor = prefs.edit();
- editor.putLong(
- PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
- -1);
- editor.commit();
- }
-
- // delete image file
- if (feed.getImage() != null) {
- if (feed.getImage().isDownloaded()
- && feed.getImage().getFile_url() != null) {
- File imageFile = new File(feed.getImage()
- .getFile_url());
- imageFile.delete();
- } else if (requester.isDownloadingFile(feed.getImage())) {
- requester.cancelDownload(context, feed.getImage());
- }
- }
- // delete stored media files and mark them as read
- List<FeedItem> queue = DBReader.getQueue(context);
- boolean queueWasModified = false;
- if (feed.getItems() == null) {
- DBReader.getFeedItemList(context, feed);
- }
-
- for (FeedItem item : feed.getItems()) {
- queueWasModified |= queue.remove(item);
- if (item.getMedia() != null
- && item.getMedia().isDownloaded()) {
- File mediaFile = new File(item.getMedia()
- .getFile_url());
- mediaFile.delete();
- } else if (item.getMedia() != null
- && requester.isDownloadingFile(item.getMedia())) {
- requester.cancelDownload(context, item.getMedia());
- }
-
- if (item.hasItemImage()) {
- FeedImage image = item.getImage();
- if (image.isDownloaded() && image.getFile_url() != null) {
- File imgFile = new File(image.getFile_url());
- imgFile.delete();
- } else if (requester.isDownloadingFile(image)) {
- requester.cancelDownload(context, item.getImage());
- }
- }
- }
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- if (queueWasModified) {
- adapter.setQueue(queue);
- }
- adapter.removeFeed(feed);
- adapter.close();
-
- GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
- EventDistributor.getInstance().sendFeedUpdateBroadcast();
-
- BackupManager backupManager = new BackupManager(context);
- backupManager.dataChanged();
- }
- }
- });
- }
-
- /**
- * Deletes the entire playback history.
- *
- * @param context A context that is used for opening a database connection.
- */
- public static Future<?> clearPlaybackHistory(final Context context) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.clearPlaybackHistory();
- adapter.close();
- EventDistributor.getInstance()
- .sendPlaybackHistoryUpdateBroadcast();
- }
- });
- }
-
- /**
- * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if
- * its playback completion date is set to a non-null value. This method will set the playback completion date to the
- * current date regardless of the current value.
- *
- * @param context A context that is used for opening a database connection.
- * @param media FeedMedia that should be added to the playback history.
- */
- public static Future<?> addItemToPlaybackHistory(final Context context,
- final FeedMedia media) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Adding new item to playback history");
- media.setPlaybackCompletionDate(new Date());
- // reset played_duration to 0 so that it behaves correctly when the episode is played again
- media.setPlayedDuration(0);
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedMediaPlaybackCompletionDate(media);
- adapter.close();
- EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast();
-
- }
- });
- }
-
- private static void cleanupDownloadLog(final PodDBAdapter adapter) {
- final long logSize = adapter.getDownloadLogSize();
- if (logSize > DBReader.DOWNLOAD_LOG_SIZE) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Cleaning up download log");
- adapter.removeDownloadLogItems(logSize - DBReader.DOWNLOAD_LOG_SIZE);
- }
- }
-
- /**
- * Adds a Download status object to the download log.
- *
- * @param context A context that is used for opening a database connection.
- * @param status The DownloadStatus object.
- */
- public static Future<?> addDownloadStatus(final Context context,
- final DownloadStatus status) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
-
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setDownloadStatus(status);
- adapter.close();
- EventDistributor.getInstance().sendDownloadLogUpdateBroadcast();
- }
- });
-
- }
-
- /**
- * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to
- * true. If the FeedItem is already in the queue, the queue will not be modified.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId ID of the FeedItem that should be added to the queue.
- * @param index Destination index. Must be in range 0..queue.size()
- * @param performAutoDownload True if an auto-download process should be started after the operation
- * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size()
- */
- public static Future<?> addQueueItemAt(final Context context, final long itemId,
- final int index, final boolean performAutoDownload) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader
- .getQueue(context, adapter);
- FeedItem item = null;
-
- if (queue != null) {
- boolean queueModified = false;
- boolean unreadItemsModified = false;
-
- if (!itemListContains(queue, itemId)) {
- item = DBReader.getFeedItem(context, itemId);
- if (item != null) {
- queue.add(index, item);
- queueModified = true;
- if (!item.isRead()) {
- item.setRead(true);
- unreadItemsModified = true;
- }
- }
- }
- if (queueModified) {
- adapter.setQueue(queue);
- EventDistributor.getInstance()
- .sendQueueUpdateBroadcast();
- }
- if (unreadItemsModified && item != null) {
- adapter.setSingleFeedItem(item);
- EventDistributor.getInstance()
- .sendUnreadItemsUpdateBroadcast();
- }
- }
- adapter.close();
- if (performAutoDownload) {
- DBTasks.autodownloadUndownloadedItems(context);
- }
-
- }
- });
-
- }
-
- /**
- * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true.
- * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemIds IDs of the FeedItem objects that should be added to the queue.
- */
- public static Future<?> addQueueItem(final Context context,
- final long... itemIds) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- if (itemIds.length > 0) {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader.getQueue(context,
- adapter);
-
- if (queue != null) {
- boolean queueModified = false;
- boolean unreadItemsModified = false;
- List<FeedItem> itemsToSave = new LinkedList<FeedItem>();
- for (int i = 0; i < itemIds.length; i++) {
- if (!itemListContains(queue, itemIds[i])) {
- final FeedItem item = DBReader.getFeedItem(
- context, itemIds[i]);
-
- if (item != null) {
- queue.add(item);
- queueModified = true;
- if (!item.isRead()) {
- item.setRead(true);
- itemsToSave.add(item);
- unreadItemsModified = true;
- }
- }
- }
- }
- if (queueModified) {
- adapter.setQueue(queue);
- EventDistributor.getInstance()
- .sendQueueUpdateBroadcast();
- }
- if (unreadItemsModified) {
- adapter.setFeedItemlist(itemsToSave);
- EventDistributor.getInstance()
- .sendUnreadItemsUpdateBroadcast();
- }
- }
- adapter.close();
- DBTasks.autodownloadUndownloadedItems(context);
- }
- }
- });
-
- }
-
- /**
- * Removes all FeedItem objects from the queue.
- *
- * @param context A context that is used for opening a database connection.
- */
- public static Future<?> clearQueue(final Context context) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.clearQueue();
- adapter.close();
-
- EventDistributor.getInstance().sendQueueUpdateBroadcast();
- }
- });
- }
-
- /**
- * Removes a FeedItem object from the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId ID of the FeedItem that should be removed.
- * @param performAutoDownload true if an auto-download process should be started after the operation.
- */
- public static Future<?> removeQueueItem(final Context context,
- final long itemId, final boolean performAutoDownload) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader
- .getQueue(context, adapter);
- FeedItem item = null;
-
- if (queue != null) {
- boolean queueModified = false;
- QueueAccess queueAccess = QueueAccess.ItemListAccess(queue);
- if (queueAccess.contains(itemId)) {
- item = DBReader.getFeedItem(context, itemId);
- if (item != null) {
- queueModified = queueAccess.remove(itemId);
- }
- }
- if (queueModified) {
- adapter.setQueue(queue);
- EventDistributor.getInstance()
- .sendQueueUpdateBroadcast();
- } else {
- Log.w(TAG, "Queue was not modified by call to removeQueueItem");
- }
- } else {
- Log.e(TAG, "removeQueueItem: Could not load queue");
- }
- adapter.close();
- if (performAutoDownload) {
- DBTasks.autodownloadUndownloadedItems(context);
- }
- }
- });
-
- }
-
- /**
- * Moves the specified item to the top of the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId The item to move to the top of the queue
- * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
- * false if the caller wants to avoid unexpected updates of the GUI.
- */
- public static Future<?> moveQueueItemToTop(final Context context, final long itemId, final boolean broadcastUpdate) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- List<Long> queueIdList = DBReader.getQueueIDList(context);
- int currentLocation = 0;
- for (long id : queueIdList) {
- if (id == itemId) {
- moveQueueItemHelper(context, currentLocation, 0, broadcastUpdate);
- return;
- }
- currentLocation++;
- }
- Log.e(TAG, "moveQueueItemToTop: item not found");
- }
- });
- }
-
- /**
- * Moves the specified item to the bottom of the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId The item to move to the bottom of the queue
- * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
- * false if the caller wants to avoid unexpected updates of the GUI.
- */
- public static Future<?> moveQueueItemToBottom(final Context context, final long itemId,
- final boolean broadcastUpdate) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- List<Long> queueIdList = DBReader.getQueueIDList(context);
- int currentLocation = 0;
- for (long id : queueIdList) {
- if (id == itemId) {
- moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1,
- broadcastUpdate);
- return;
- }
- currentLocation++;
- }
- Log.e(TAG, "moveQueueItemToBottom: item not found");
- }
- });
- }
-
- /**
- * Changes the position of a FeedItem in the queue.
- *
- * @param context A context that is used for opening a database connection.
- * @param from Source index. Must be in range 0..queue.size()-1.
- * @param to Destination index. Must be in range 0..queue.size()-1.
- * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
- * false if the caller wants to avoid unexpected updates of the GUI.
- * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size())
- */
- public static Future<?> moveQueueItem(final Context context, final int from,
- final int to, final boolean broadcastUpdate) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- moveQueueItemHelper(context, from, to, broadcastUpdate);
- }
- });
- }
-
- /**
- * Changes the position of a FeedItem in the queue.
- * <p/>
- * This function must be run using the ExecutorService (dbExec).
- *
- * @param context A context that is used for opening a database connection.
- * @param from Source index. Must be in range 0..queue.size()-1.
- * @param to Destination index. Must be in range 0..queue.size()-1.
- * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to
- * false if the caller wants to avoid unexpected updates of the GUI.
- * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size())
- */
- private static void moveQueueItemHelper(final Context context, final int from,
- final int to, final boolean broadcastUpdate) {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- final List<FeedItem> queue = DBReader
- .getQueue(context, adapter);
-
- if (queue != null) {
- if (from >= 0 && from < queue.size() && to >= 0
- && to < queue.size()) {
-
- final FeedItem item = queue.remove(from);
- queue.add(to, item);
-
- adapter.setQueue(queue);
- if (broadcastUpdate) {
- EventDistributor.getInstance()
- .sendQueueUpdateBroadcast();
- }
-
- }
- } else {
- Log.e(TAG, "moveQueueItemHelper: Could not load queue");
- }
- adapter.close();
- }
-
- /**
- * Sets the 'read'-attribute of a FeedItem to the specified value.
- *
- * @param context A context that is used for opening a database connection.
- * @param item The FeedItem object
- * @param read New value of the 'read'-attribute
- * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object.
- * If the FeedItem has no FeedMedia object, this parameter will be ignored.
- */
- public static Future<?> markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) {
- long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0;
- return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition);
- }
-
- /**
- * Sets the 'read'-attribute of a FeedItem to the specified value.
- *
- * @param context A context that is used for opening a database connection.
- * @param itemId ID of the FeedItem
- * @param read New value of the 'read'-attribute
- */
- public static Future<?> markItemRead(final Context context, final long itemId,
- final boolean read) {
- return markItemRead(context, itemId, read, 0, false);
- }
-
- private static Future<?> markItemRead(final Context context, final long itemId,
- final boolean read, final long mediaId,
- final boolean resetMediaPosition) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedItemRead(read, itemId, mediaId,
- resetMediaPosition);
- adapter.close();
-
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
- }
- });
- }
-
- /**
- * Sets the 'read'-attribute of all FeedItems of a specific Feed to true.
- *
- * @param context A context that is used for opening a database connection.
- * @param feedId ID of the Feed.
- */
- public static Future<?> markFeedRead(final Context context, final long feedId) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId);
- long[] itemIds = new long[itemCursor.getCount()];
- itemCursor.moveToFirst();
- for (int i = 0; i < itemIds.length; i++) {
- itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- itemCursor.moveToNext();
- }
- itemCursor.close();
- adapter.setFeedItemRead(true, itemIds);
- adapter.close();
-
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
- }
- });
-
- }
-
- /**
- * Sets the 'read'-attribute of all FeedItems to true.
- *
- * @param context A context that is used for opening a database connection.
- */
- public static Future<?> markAllItemsRead(final Context context) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- Cursor itemCursor = adapter.getUnreadItemsCursor();
- long[] itemIds = new long[itemCursor.getCount()];
- itemCursor.moveToFirst();
- for (int i = 0; i < itemIds.length; i++) {
- itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX);
- itemCursor.moveToNext();
- }
- itemCursor.close();
- adapter.setFeedItemRead(true, itemIds);
- adapter.close();
-
- EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
- }
- });
-
- }
-
- static Future<?> addNewFeed(final Context context, final Feed... feeds) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- final PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setCompleteFeed(feeds);
- adapter.close();
-
- for (Feed feed : feeds) {
- GpodnetPreferences.addAddedFeed(feed.getDownload_url());
- }
-
- BackupManager backupManager = new BackupManager(context);
- backupManager.dataChanged();
- }
- });
- }
-
- static Future<?> setCompleteFeed(final Context context, final Feed... feeds) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setCompleteFeed(feeds);
- adapter.close();
-
- }
- });
-
- }
-
- /**
- * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The
- * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved.
- *
- * @param context A context that is used for opening a database connection.
- * @param media The FeedMedia object.
- */
- public static Future<?> setFeedMedia(final Context context,
- final FeedMedia media) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setMedia(media);
- adapter.close();
- }
- });
- }
-
- /**
- * Saves the 'position' and 'duration' attributes of a FeedMedia object
- *
- * @param context A context that is used for opening a database connection.
- * @param media The FeedMedia object.
- */
- public static Future<?> setFeedMediaPlaybackInformation(final Context context, final FeedMedia media) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedMediaPlaybackInformation(media);
- adapter.close();
- }
- });
- }
-
- /**
- * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including
- * the content of FeedComponent-attributes.
- *
- * @param context A context that is used for opening a database connection.
- * @param item The FeedItem object.
- */
- public static Future<?> setFeedItem(final Context context,
- final FeedItem item) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setSingleFeedItem(item);
- adapter.close();
- }
- });
- }
-
- /**
- * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The
- * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved.
- *
- * @param context A context that is used for opening a database connection.
- * @param image The FeedImage object.
- */
- public static Future<?> setFeedImage(final Context context,
- final FeedImage image) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setImage(image);
- adapter.close();
- }
- });
- }
-
- /**
- * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed
- * and the value is the updated URL
- */
- public static Future<?> updateFeedDownloadURLs(final Context context, final Map<String, String> urls) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- for (String key : urls.keySet()) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key));
-
- adapter.setFeedDownloadUrl(key, urls.get(key));
- }
- adapter.close();
- }
- });
- }
-
- /**
- * Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0.
- *
- * @param context Used for opening a database connection.
- * @param preferences The FeedPreferences object.
- */
- public static Future<?> setFeedPreferences(final Context context, final FeedPreferences preferences) {
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedPreferences(preferences);
- adapter.close();
- EventDistributor.getInstance().sendFeedUpdateBroadcast();
- }
- });
- }
-
- private static boolean itemListContains(List<FeedItem> items, long itemId) {
- for (FeedItem item : items) {
- if (item.getId() == itemId) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Saves the FlattrStatus of a FeedItem object in the database.
- *
- * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
- */
- public static Future<?> setFeedItemFlattrStatus(final Context context,
- final FeedItem item,
- final boolean startFlattrClickWorker) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedItemFlattrStatus(item);
- adapter.close();
- if (startFlattrClickWorker) {
- new FlattrClickWorker(context).executeAsync();
- }
- }
- });
- }
-
- /**
- * Saves the FlattrStatus of a Feed object in the database.
- *
- * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
- */
- private static Future<?> setFeedFlattrStatus(final Context context,
- final Feed feed,
- final boolean startFlattrClickWorker) {
- return dbExec.submit(new Runnable() {
-
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.setFeedFlattrStatus(feed);
- adapter.close();
- if (startFlattrClickWorker) {
- new FlattrClickWorker(context).executeAsync();
- }
- }
- });
- }
-
- /**
- * format an url for querying the database
- * (postfix a / and apply percent-encoding)
- */
- private static String formatURIForQuery(String uri) {
- try {
- return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- Log.e(TAG, e.getMessage());
- return "";
- }
- }
-
-
- /**
- * Set flattr status of the passed thing (either a FeedItem or a Feed)
- *
- * @param context
- * @param thing
- * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved
- * @return
- */
- public static Future<?> setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) {
- // must propagate this to back db
- if (thing instanceof FeedItem)
- return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker);
- else if (thing instanceof Feed)
- return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker);
- else if (thing instanceof SimpleFlattrThing) {
- } // SimpleFlattrThings are generated on the fly and do not have DB backing
- else
- Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing");
-
- return null;
- }
-
- /**
- * Reset flattr status to unflattrd for all items
- */
- public static Future<?> clearAllFlattrStatus(final Context context) {
- Log.d(TAG, "clearAllFlattrStatus()");
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.clearAllFlattrStatus();
- adapter.close();
- }
- });
- }
-
- /**
- * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp,
- * where the information has been retrieved from the flattr API
- */
- public static Future<?> setFlattredStatus(final Context context, final List<Flattr> flattrList) {
- Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items");
- // clear flattr status in db
- clearAllFlattrStatus(context);
-
- // submit list with flattred things having normalized URLs to db
- return dbExec.submit(new Runnable() {
- @Override
- public void run() {
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- for (Flattr flattr : flattrList) {
- adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime()));
- }
- adapter.close();
- }
- });
- }
-}
diff --git a/src/de/danoeh/antennapod/storage/DownloadRequestException.java b/src/de/danoeh/antennapod/storage/DownloadRequestException.java
deleted file mode 100644
index 0ef766e58..000000000
--- a/src/de/danoeh/antennapod/storage/DownloadRequestException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-/**
- * Thrown by the DownloadRequester if a download request contains invalid data
- * or something went wrong while processing the request.
- */
-public class DownloadRequestException extends Exception {
-
- public DownloadRequestException() {
- super();
- }
-
- public DownloadRequestException(String detailMessage, Throwable throwable) {
- super(detailMessage, throwable);
- }
-
- public DownloadRequestException(String detailMessage) {
- super(detailMessage);
- }
-
- public DownloadRequestException(Throwable throwable) {
- super(throwable);
- }
-
-}
diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java
deleted file mode 100644
index 0eae52137..000000000
--- a/src/de/danoeh/antennapod/storage/DownloadRequester.java
+++ /dev/null
@@ -1,367 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-import android.webkit.URLUtil;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.*;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.download.DownloadRequest;
-import de.danoeh.antennapod.service.download.DownloadService;
-import de.danoeh.antennapod.util.FileNameGenerator;
-import de.danoeh.antennapod.util.URLChecker;
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.lang3.StringEscapeUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-
-import java.io.File;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-
-/**
- * Sends download requests to the DownloadService. This class should always be used for starting downloads,
- * otherwise they won't work correctly.
- */
-public class DownloadRequester {
- private static final String TAG = "DownloadRequester";
-
- public static final String IMAGE_DOWNLOADPATH = "images/";
- public static final String FEED_DOWNLOADPATH = "cache/";
- public static final String MEDIA_DOWNLOADPATH = "media/";
-
- private static DownloadRequester downloader;
-
- private Map<String, DownloadRequest> downloads;
-
- private DownloadRequester() {
- downloads = new ConcurrentHashMap<String, DownloadRequest>();
- }
-
- public static synchronized DownloadRequester getInstance() {
- if (downloader == null) {
- downloader = new DownloadRequester();
- }
- return downloader;
- }
-
- /**
- * Starts a new download with the given DownloadRequest. This method should only
- * be used from outside classes if the DownloadRequest was created by the DownloadService to
- * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead.
- *
- * @param context Context object for starting the DownloadService
- * @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method
- * call will return false.
- * @return True if the download request was accepted, false otherwise.
- */
- public synchronized boolean download(Context context, DownloadRequest request) {
- Validate.notNull(context);
- Validate.notNull(request);
-
- if (downloads.containsKey(request.getSource())) {
- if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored.");
- return false;
- }
- downloads.put(request.getSource(), request);
-
- Intent launchIntent = new Intent(context, DownloadService.class);
- launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
- context.startService(launchIntent);
- EventDistributor.getInstance().sendDownloadQueuedBroadcast();
- return true;
- }
-
- private void download(Context context, FeedFile item, File dest,
- boolean overwriteIfExists, String username, String password, boolean deleteOnFailure) {
- if (!isDownloadingFile(item)) {
- if (!isFilenameAvailable(dest.toString()) || (deleteOnFailure && dest.exists())) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Filename already used.");
- if (isFilenameAvailable(dest.toString()) && overwriteIfExists) {
- boolean result = dest.delete();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Deleting file. Result: " + result);
- } else {
- // find different name
- File newDest = null;
- for (int i = 1; i < Integer.MAX_VALUE; i++) {
- String newName = FilenameUtils.getBaseName(dest
- .getName())
- + "-"
- + i
- + FilenameUtils.EXTENSION_SEPARATOR
- + FilenameUtils.getExtension(dest.getName());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Testing filename " + newName);
- newDest = new File(dest.getParent(), newName);
- if (!newDest.exists()
- && isFilenameAvailable(newDest.toString())) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "File doesn't exist yet. Using "
- + newName);
- break;
- }
- }
- if (newDest != null) {
- dest = newDest;
- }
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Requesting download of url " + item.getDownload_url());
- item.setDownload_url(URLChecker.prepareURL(item.getDownload_url()));
-
- DownloadRequest request = new DownloadRequest(dest.toString(),
- URLChecker.prepareURL(item.getDownload_url()), item.getHumanReadableIdentifier(),
- item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure);
-
- download(context, request);
- } else {
- Log.e(TAG, "URL " + item.getDownload_url()
- + " is already being downloaded");
- }
- }
-
- /**
- * Returns true if a filename is available and false if it has already been
- * taken by another requested download.
- */
- private boolean isFilenameAvailable(String path) {
- for (String key : downloads.keySet()) {
- DownloadRequest r = downloads.get(key);
- if (StringUtils.equals(r.getDestination(), path)) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, path
- + " is already used by another requested download");
- return false;
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, path + " is available as a download destination");
- return true;
- }
-
- public synchronized void downloadFeed(Context context, Feed feed)
- throws DownloadRequestException {
- if (feedFileValid(feed)) {
- String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null;
- String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null;
-
- download(context, feed, new File(getFeedfilePath(context),
- getFeedfileName(feed)), true, username, password, true);
- }
- }
-
- public synchronized void downloadImage(Context context, FeedImage image)
- throws DownloadRequestException {
- if (feedFileValid(image)) {
- download(context, image, new File(getImagefilePath(context),
- getImagefileName(image)), false, null, null, false);
- }
- }
-
- public synchronized void downloadMedia(Context context, FeedMedia feedmedia)
- throws DownloadRequestException {
- if (feedFileValid(feedmedia)) {
- Feed feed = feedmedia.getItem().getFeed();
- String username;
- String password;
- if (feed != null && feed.getPreferences() != null) {
- username = feed.getPreferences().getUsername();
- password = feed.getPreferences().getPassword();
- } else {
- username = null;
- password = null;
- }
-
- File dest;
- if (feedmedia.getFile_url() != null) {
- dest = new File(feedmedia.getFile_url());
- } else {
- dest = new File(getMediafilePath(context, feedmedia),
- getMediafilename(feedmedia));
- }
- download(context, feedmedia,
- dest, false, username, password, false
- );
- }
- }
-
- /**
- * Throws a DownloadRequestException if the feedfile or the download url of
- * the feedfile is null.
- *
- * @throws DownloadRequestException
- */
- private boolean feedFileValid(FeedFile f) throws DownloadRequestException {
- if (f == null) {
- throw new DownloadRequestException("Feedfile was null");
- } else if (f.getDownload_url() == null) {
- throw new DownloadRequestException("File has no download URL");
- } else {
- return true;
- }
- }
-
- /**
- * Cancels a running download.
- */
- public synchronized void cancelDownload(final Context context, final FeedFile f) {
- cancelDownload(context, f.getDownload_url());
- }
-
- /**
- * Cancels a running download.
- */
- public synchronized void cancelDownload(final Context context, final String downloadUrl) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Cancelling download with url " + downloadUrl);
- Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD);
- cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl);
- context.sendBroadcast(cancelIntent);
- }
-
- /**
- * Cancels all running downloads
- */
- public synchronized void cancelAllDownloads(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Cancelling all running downloads");
- context.sendBroadcast(new Intent(
- DownloadService.ACTION_CANCEL_ALL_DOWNLOADS));
- }
-
- /**
- * Returns true if there is at least one Feed in the downloads queue.
- */
- public synchronized boolean isDownloadingFeeds() {
- for (DownloadRequest r : downloads.values()) {
- if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Checks if feedfile is in the downloads list
- */
- public synchronized boolean isDownloadingFile(FeedFile item) {
- if (item.getDownload_url() != null) {
- return downloads.containsKey(item.getDownload_url());
- }
- return false;
- }
-
- public synchronized DownloadRequest getDownload(String downloadUrl) {
- return downloads.get(downloadUrl);
- }
-
- /**
- * Checks if feedfile with the given download url is in the downloads list
- */
- public synchronized boolean isDownloadingFile(String downloadUrl) {
- return downloads.get(downloadUrl) != null;
- }
-
- public synchronized boolean hasNoDownloads() {
- return downloads.isEmpty();
- }
-
- /**
- * Remove an object from the downloads-list of the requester.
- */
- public synchronized void removeDownload(DownloadRequest r) {
- if (downloads.remove(r.getSource()) == null) {
- Log.e(TAG,
- "Could not remove object with url " + r.getSource());
- }
- }
-
- /**
- * Get the number of uncompleted Downloads
- */
- public synchronized int getNumberOfDownloads() {
- return downloads.size();
- }
-
- public synchronized String getFeedfilePath(Context context)
- throws DownloadRequestException {
- return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH)
- .toString() + "/";
- }
-
- public synchronized String getFeedfileName(Feed feed) {
- String filename = feed.getDownload_url();
- if (feed.getTitle() != null && !feed.getTitle().isEmpty()) {
- filename = feed.getTitle();
- }
- return "feed-" + FileNameGenerator.generateFileName(filename);
- }
-
- public synchronized String getImagefilePath(Context context)
- throws DownloadRequestException {
- return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH)
- .toString() + "/";
- }
-
- public synchronized String getImagefileName(FeedImage image) {
- String filename = image.getDownload_url();
- if (image.getOwner() != null && image.getOwner().getHumanReadableIdentifier() != null) {
- filename = image.getOwner().getHumanReadableIdentifier();
- }
- return "image-" + FileNameGenerator.generateFileName(filename);
- }
-
- public synchronized String getMediafilePath(Context context, FeedMedia media)
- throws DownloadRequestException {
- File externalStorage = getExternalFilesDirOrThrowException(
- context,
- MEDIA_DOWNLOADPATH
- + FileNameGenerator.generateFileName(media.getItem()
- .getFeed().getTitle()) + "/"
- );
- return externalStorage.toString();
- }
-
- private File getExternalFilesDirOrThrowException(Context context,
- String type) throws DownloadRequestException {
- File result = UserPreferences.getDataFolder(context, type);
- if (result == null) {
- throw new DownloadRequestException(
- "Failed to access external storage");
- }
- return result;
- }
-
- private String getMediafilename(FeedMedia media) {
- String filename;
- String titleBaseFilename = "";
-
- // Try to generate the filename by the item title
- if (media.getItem() != null && media.getItem().getTitle() != null) {
- String title = media.getItem().getTitle();
- // Delete reserved characters
- titleBaseFilename = title.replaceAll("[\\\\/%\\?\\*:|<>\"\\p{Cntrl}]", "");
- titleBaseFilename = titleBaseFilename.trim();
- }
-
- String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(),
- null, media.getMime_type());
- ;
-
- if (titleBaseFilename != "") {
- // Append extension
- filename = titleBaseFilename + FilenameUtils.EXTENSION_SEPARATOR +
- FilenameUtils.getExtension(URLBaseFilename);
- } else {
- // Fall back on URL file name
- filename = URLBaseFilename;
- }
- return filename;
- }
-}
diff --git a/src/de/danoeh/antennapod/storage/FeedItemStatistics.java b/src/de/danoeh/antennapod/storage/FeedItemStatistics.java
deleted file mode 100644
index 8cb040756..000000000
--- a/src/de/danoeh/antennapod/storage/FeedItemStatistics.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-import java.util.Date;
-
-/**
- * Contains information about a feed's items.
- */
-public class FeedItemStatistics {
- private long feedID;
- private int numberOfItems;
- private int numberOfNewItems;
- private int numberOfInProgressItems;
- private Date lastUpdate;
- private static final Date UNKNOWN_DATE = new Date(0);
-
-
- /**
- * Creates new FeedItemStatistics object.
- *
- * @param feedID ID of the feed.
- * @param numberOfItems Number of items that this feed has.
- * @param numberOfNewItems Number of unread items this feed has.
- * @param numberOfInProgressItems Number of items that the user has started listening to.
- * @param lastUpdate pubDate of the latest episode. A lastUpdate value of 0 will be interpreted as DATE_UNKOWN if
- * numberOfItems is 0.
- */
- public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) {
- this.feedID = feedID;
- this.numberOfItems = numberOfItems;
- this.numberOfNewItems = numberOfNewItems;
- this.numberOfInProgressItems = numberOfInProgressItems;
- if (numberOfItems > 0) {
- this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
- } else {
- this.lastUpdate = UNKNOWN_DATE;
- }
- }
-
- public long getFeedID() {
- return feedID;
- }
-
- public int getNumberOfItems() {
- return numberOfItems;
- }
-
- public int getNumberOfNewItems() {
- return numberOfNewItems;
- }
-
- public int getNumberOfInProgressItems() {
- return numberOfInProgressItems;
- }
-
- /**
- * Returns the pubDate of the latest item in the feed. Users of this method
- * should check if this value is unkown or not by calling lastUpdateKnown() first.
- */
- public Date getLastUpdate() {
- return (lastUpdate != null) ? (Date) lastUpdate.clone() : null;
- }
-
- /**
- * Returns true if the lastUpdate value is known. The lastUpdate value is unkown if the
- * feed has no items.
- */
- public boolean lastUpdateKnown() {
- return lastUpdate != UNKNOWN_DATE;
- }
-}
diff --git a/src/de/danoeh/antennapod/storage/FeedSearcher.java b/src/de/danoeh/antennapod/storage/FeedSearcher.java
deleted file mode 100644
index e7aa93f83..000000000
--- a/src/de/danoeh/antennapod/storage/FeedSearcher.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-import android.content.Context;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.SearchResult;
-import de.danoeh.antennapod.util.comparator.SearchResultValueComparator;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-
-/**
- * Performs search on Feeds and FeedItems
- */
-public class FeedSearcher {
- private static final String TAG = "FeedSearcher";
-
-
- /**
- * Performs a search in all feeds or one specific feed.
- */
- public static List<SearchResult> performSearch(final Context context,
- final String query, final long selectedFeed) {
- final int values[] = {0, 0, 1, 2};
- final String[] subtitles = {context.getString(R.string.found_in_shownotes_label),
- context.getString(R.string.found_in_shownotes_label),
- context.getString(R.string.found_in_chapters_label),
- context.getString(R.string.found_in_title_label)};
-
- List<SearchResult> result = new ArrayList<SearchResult>();
-
- FutureTask<List<FeedItem>>[] tasks = new FutureTask[4];
- (tasks[0] = DBTasks.searchFeedItemContentEncoded(context, selectedFeed, query)).run();
- (tasks[1] = DBTasks.searchFeedItemDescription(context, selectedFeed, query)).run();
- (tasks[2] = DBTasks.searchFeedItemChapters(context, selectedFeed, query)).run();
- (tasks[3] = DBTasks.searchFeedItemTitle(context, selectedFeed, query)).run();
- try {
- for (int i = 0; i < tasks.length; i++) {
- FutureTask task = tasks[i];
- List<FeedItem> items = (List<FeedItem>) task.get();
- for (FeedItem item : items) {
- result.add(new SearchResult(item, values[i], subtitles[i]));
- }
-
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- Collections.sort(result, new SearchResultValueComparator());
- return result;
- }
-}
diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java
deleted file mode 100644
index 671ac30d5..000000000
--- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java
+++ /dev/null
@@ -1,1391 +0,0 @@
-package de.danoeh.antennapod.storage;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.MergeCursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteDatabase.CursorFactory;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
-
-import org.apache.commons.lang3.Validate;
-
-import java.util.Arrays;
-import java.util.List;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedComponent;
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.feed.FeedPreferences;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-
-// TODO Remove media column from feeditem table
-
-/**
- * Implements methods for accessing the database
- */
-public class PodDBAdapter {
- private static final String TAG = "PodDBAdapter";
- private static final int DATABASE_VERSION = 12;
- public static final String DATABASE_NAME = "Antennapod.db";
-
- /**
- * Maximum number of arguments for IN-operator.
- */
- public static final int IN_OPERATOR_MAXIMUM = 800;
-
- /**
- * Maximum number of entries per search request.
- */
- public static final int SEARCH_LIMIT = 30;
-
- // ----------- Column indices
- // ----------- General indices
- public static final int KEY_ID_INDEX = 0;
- public static final int KEY_TITLE_INDEX = 1;
- public static final int KEY_FILE_URL_INDEX = 2;
- public static final int KEY_DOWNLOAD_URL_INDEX = 3;
- public static final int KEY_DOWNLOADED_INDEX = 4;
- public static final int KEY_LINK_INDEX = 5;
- public static final int KEY_DESCRIPTION_INDEX = 6;
- public static final int KEY_PAYMENT_LINK_INDEX = 7;
- // ----------- Feed indices
- public static final int KEY_LAST_UPDATE_INDEX = 8;
- public static final int KEY_LANGUAGE_INDEX = 9;
- public static final int KEY_AUTHOR_INDEX = 10;
- public static final int KEY_IMAGE_INDEX = 11;
- public static final int KEY_TYPE_INDEX = 12;
- public static final int KEY_FEED_IDENTIFIER_INDEX = 13;
- public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14;
- public static final int KEY_FEED_USERNAME_INDEX = 15;
- public static final int KEY_FEED_PASSWORD_INDEX = 16;
- // ----------- FeedItem indices
- public static final int KEY_CONTENT_ENCODED_INDEX = 2;
- public static final int KEY_PUBDATE_INDEX = 3;
- public static final int KEY_READ_INDEX = 4;
- public static final int KEY_MEDIA_INDEX = 8;
- public static final int KEY_FEED_INDEX = 9;
- public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10;
- public static final int KEY_ITEM_IDENTIFIER_INDEX = 11;
- public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12;
- // ---------- FeedMedia indices
- public static final int KEY_DURATION_INDEX = 1;
- public static final int KEY_POSITION_INDEX = 5;
- public static final int KEY_SIZE_INDEX = 6;
- public static final int KEY_MIME_TYPE_INDEX = 7;
- public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8;
- public static final int KEY_MEDIA_FEEDITEM_INDEX = 9;
- public static final int KEY_PLAYED_DURATION_INDEX = 10;
- // --------- Download log indices
- public static final int KEY_FEEDFILE_INDEX = 1;
- public static final int KEY_FEEDFILETYPE_INDEX = 2;
- public static final int KEY_REASON_INDEX = 3;
- public static final int KEY_SUCCESSFUL_INDEX = 4;
- public static final int KEY_COMPLETION_DATE_INDEX = 5;
- public static final int KEY_REASON_DETAILED_INDEX = 6;
- public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7;
- // --------- Queue indices
- public static final int KEY_FEEDITEM_INDEX = 1;
- public static final int KEY_QUEUE_FEED_INDEX = 2;
- // --------- Chapters indices
- public static final int KEY_CHAPTER_START_INDEX = 2;
- public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3;
- public static final int KEY_CHAPTER_LINK_INDEX = 4;
- public static final int KEY_CHAPTER_TYPE_INDEX = 5;
-
- // Key-constants
- public static final String KEY_ID = "id";
- public static final String KEY_TITLE = "title";
- public static final String KEY_NAME = "name";
- public static final String KEY_LINK = "link";
- public static final String KEY_DESCRIPTION = "description";
- public static final String KEY_FILE_URL = "file_url";
- public static final String KEY_DOWNLOAD_URL = "download_url";
- public static final String KEY_PUBDATE = "pubDate";
- public static final String KEY_READ = "read";
- public static final String KEY_DURATION = "duration";
- public static final String KEY_POSITION = "position";
- public static final String KEY_SIZE = "filesize";
- public static final String KEY_MIME_TYPE = "mime_type";
- public static final String KEY_IMAGE = "image";
- public static final String KEY_FEED = "feed";
- public static final String KEY_MEDIA = "media";
- public static final String KEY_DOWNLOADED = "downloaded";
- public static final String KEY_LASTUPDATE = "last_update";
- public static final String KEY_FEEDFILE = "feedfile";
- public static final String KEY_REASON = "reason";
- public static final String KEY_SUCCESSFUL = "successful";
- public static final String KEY_FEEDFILETYPE = "feedfile_type";
- public static final String KEY_COMPLETION_DATE = "completion_date";
- public static final String KEY_FEEDITEM = "feeditem";
- public static final String KEY_CONTENT_ENCODED = "content_encoded";
- public static final String KEY_PAYMENT_LINK = "payment_link";
- public static final String KEY_START = "start";
- public static final String KEY_LANGUAGE = "language";
- public static final String KEY_AUTHOR = "author";
- public static final String KEY_HAS_CHAPTERS = "has_simple_chapters";
- public static final String KEY_TYPE = "type";
- public static final String KEY_ITEM_IDENTIFIER = "item_identifier";
- public static final String KEY_FLATTR_STATUS = "flattr_status";
- public static final String KEY_FEED_IDENTIFIER = "feed_identifier";
- public static final String KEY_REASON_DETAILED = "reason_detailed";
- public static final String KEY_DOWNLOADSTATUS_TITLE = "title";
- public static final String KEY_CHAPTER_TYPE = "type";
- public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date";
- public static final String KEY_AUTO_DOWNLOAD = "auto_download";
- public static final String KEY_PLAYED_DURATION = "played_duration";
- public static final String KEY_USERNAME = "username";
- public static final String KEY_PASSWORD = "password";
-
- // Table names
- public static final String TABLE_NAME_FEEDS = "Feeds";
- public static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
- public static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
- public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
- public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
- public static final String TABLE_NAME_QUEUE = "Queue";
- public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
-
- // SQL Statements for creating new tables
- private static final String TABLE_PRIMARY_KEY = KEY_ID
- + " INTEGER PRIMARY KEY AUTOINCREMENT ,";
-
- private static final String CREATE_TABLE_FEEDS = "CREATE TABLE "
- + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
- + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
- + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT,"
- + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
- + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR
- + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT,"
- + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1,"
- + KEY_FLATTR_STATUS + " INTEGER,"
- + KEY_USERNAME + " TEXT,"
- + KEY_PASSWORD + " TEXT)";
-
- private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE "
- + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
- + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE
- + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT,"
- + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
- + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
- + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
- + KEY_FLATTR_STATUS + " INTEGER,"
- + KEY_IMAGE + " INTEGER)";
-
- private static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE "
- + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
- + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
- + KEY_DOWNLOADED + " INTEGER)";
-
- private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
- + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
- + " INTEGER," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL
- + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION
- + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT,"
- + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER,"
- + KEY_FEEDITEM + " INTEGER,"
- + KEY_PLAYED_DURATION + " INTEGER)";
-
- private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE "
- + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE
- + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON
- + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE
- + " INTEGER," + KEY_REASON_DETAILED + " TEXT,"
- + KEY_DOWNLOADSTATUS_TITLE + " TEXT)";
-
- private static final String CREATE_TABLE_QUEUE = "CREATE TABLE "
- + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY,"
- + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)";
-
- private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE "
- + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
- + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER,"
- + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)";
-
- private SQLiteDatabase db;
- private final Context context;
- private PodDBHelper helper;
-
- /**
- * Select all columns from the feed-table
- */
- private static final String[] FEED_SEL_STD = {
- TABLE_NAME_FEEDS + "." + KEY_ID,
- TABLE_NAME_FEEDS + "." + KEY_TITLE,
- TABLE_NAME_FEEDS + "." + KEY_FILE_URL,
- TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL,
- TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED,
- TABLE_NAME_FEEDS + "." + KEY_LINK,
- TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION,
- TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK,
- TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE,
- TABLE_NAME_FEEDS + "." + KEY_LANGUAGE,
- TABLE_NAME_FEEDS + "." + KEY_AUTHOR,
- TABLE_NAME_FEEDS + "." + KEY_IMAGE,
- TABLE_NAME_FEEDS + "." + KEY_TYPE,
- TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
- TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD,
- TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS,
- TABLE_NAME_FEEDS + "." + KEY_USERNAME,
- TABLE_NAME_FEEDS + "." + KEY_PASSWORD
- };
-
- // column indices for FEED_SEL_STD
- public static final int IDX_FEED_SEL_STD_ID = 0;
- public static final int IDX_FEED_SEL_STD_TITLE = 1;
- public static final int IDX_FEED_SEL_STD_FILE_URL = 2;
- public static final int IDX_FEED_SEL_STD_DOWNLOAD_URL = 3;
- public static final int IDX_FEED_SEL_STD_DOWNLOADED = 4;
- public static final int IDX_FEED_SEL_STD_LINK = 5;
- public static final int IDX_FEED_SEL_STD_DESCRIPTION = 6;
- public static final int IDX_FEED_SEL_STD_PAYMENT_LINK = 7;
- public static final int IDX_FEED_SEL_STD_LASTUPDATE = 8;
- public static final int IDX_FEED_SEL_STD_LANGUAGE = 9;
- public static final int IDX_FEED_SEL_STD_AUTHOR = 10;
- public static final int IDX_FEED_SEL_STD_IMAGE = 11;
- public static final int IDX_FEED_SEL_STD_TYPE = 12;
- public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13;
- public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14;
- public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15;
- public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 16;
- public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 17;
-
-
- /**
- * Select all columns from the feeditems-table except description and
- * content-encoded.
- */
- private static final String[] FEEDITEM_SEL_FI_SMALL = {
- TABLE_NAME_FEED_ITEMS + "." + KEY_ID,
- TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE,
- TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE,
- TABLE_NAME_FEED_ITEMS + "." + KEY_READ,
- TABLE_NAME_FEED_ITEMS + "." + KEY_LINK,
- TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA,
- TABLE_NAME_FEED_ITEMS + "." + KEY_FEED,
- TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS,
- TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER,
- TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS,
- TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE};
-
- /**
- * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries.
- */
- private static final String SEL_FI_SMALL_STR;
-
- static {
- String selFiSmall = Arrays.toString(FEEDITEM_SEL_FI_SMALL);
- SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1);
- }
-
- // column indices for FEEDITEM_SEL_FI_SMALL
-
- public static final int IDX_FI_SMALL_ID = 0;
- public static final int IDX_FI_SMALL_TITLE = 1;
- public static final int IDX_FI_SMALL_PUBDATE = 2;
- public static final int IDX_FI_SMALL_READ = 3;
- public static final int IDX_FI_SMALL_LINK = 4;
- public static final int IDX_FI_SMALL_PAYMENT_LINK = 5;
- public static final int IDX_FI_SMALL_MEDIA = 6;
- public static final int IDX_FI_SMALL_FEED = 7;
- public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8;
- public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9;
- public static final int IDX_FI_SMALL_FLATTR_STATUS = 10;
- public static final int IDX_FI_SMALL_IMAGE = 11;
-
- /**
- * Select id, description and content-encoded column from feeditems.
- */
- private static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION,
- KEY_CONTENT_ENCODED, KEY_FEED};
-
- // column indices for SEL_FI_EXTRA
-
- public static final int IDX_FI_EXTRA_ID = 0;
- public static final int IDX_FI_EXTRA_DESCRIPTION = 1;
- public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2;
- public static final int IDX_FI_EXTRA_FEED = 3;
-
- static PodDBHelper dbHelperSingleton;
-
- private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) {
- if (dbHelperSingleton == null) {
- dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null, DATABASE_VERSION);
- }
- return dbHelperSingleton;
- }
-
- public PodDBAdapter(Context c) {
- this.context = c;
- helper = getDbHelperSingleton(c.getApplicationContext());
- }
-
- public PodDBAdapter open() {
- if (db == null || !db.isOpen() || db.isReadOnly()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Opening DB");
- try {
- db = helper.getWritableDatabase();
- } catch (SQLException ex) {
- ex.printStackTrace();
- db = helper.getReadableDatabase();
- }
- }
- return this;
- }
-
- public void close() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Closing DB");
- //db.close();
- }
-
- public static boolean deleteDatabase(Context context) {
- Log.w(TAG, "Deleting database");
- dbHelperSingleton.close();
- dbHelperSingleton = null;
- return context.deleteDatabase(DATABASE_NAME);
- }
-
- /**
- * Inserts or updates a feed entry
- *
- * @return the id of the entry
- */
- public long setFeed(Feed feed) {
- ContentValues values = new ContentValues();
- values.put(KEY_TITLE, feed.getTitle());
- values.put(KEY_LINK, feed.getLink());
- values.put(KEY_DESCRIPTION, feed.getDescription());
- values.put(KEY_PAYMENT_LINK, feed.getPaymentLink());
- values.put(KEY_AUTHOR, feed.getAuthor());
- values.put(KEY_LANGUAGE, feed.getLanguage());
- if (feed.getImage() != null) {
- if (feed.getImage().getId() == 0) {
- setImage(feed.getImage());
- }
- values.put(KEY_IMAGE, feed.getImage().getId());
- }
-
- values.put(KEY_FILE_URL, feed.getFile_url());
- values.put(KEY_DOWNLOAD_URL, feed.getDownload_url());
- values.put(KEY_DOWNLOADED, feed.isDownloaded());
- values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime());
- values.put(KEY_TYPE, feed.getType());
- values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier());
-
- Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong());
-
- values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong());
- if (feed.getId() == 0) {
- // Create new entry
- if (BuildConfig.DEBUG)
- Log.d(this.toString(), "Inserting new Feed into db");
- feed.setId(db.insert(TABLE_NAME_FEEDS, null, values));
- } else {
- if (BuildConfig.DEBUG)
- Log.d(this.toString(), "Updating existing Feed in db");
- db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?",
- new String[]{String.valueOf(feed.getId())});
-
- }
- return feed.getId();
- }
-
- public void setFeedPreferences(FeedPreferences prefs) {
- if (prefs.getFeedID() == 0) {
- throw new IllegalArgumentException("Feed ID of preference must not be null");
- }
- ContentValues values = new ContentValues();
- values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload());
- values.put(KEY_USERNAME, prefs.getUsername());
- values.put(KEY_PASSWORD, prefs.getPassword());
- db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())});
- }
-
- /**
- * Inserts or updates an image entry
- *
- * @return the id of the entry
- */
- public long setImage(FeedImage image) {
- db.beginTransaction();
- ContentValues values = new ContentValues();
- values.put(KEY_TITLE, image.getTitle());
- values.put(KEY_DOWNLOAD_URL, image.getDownload_url());
- values.put(KEY_DOWNLOADED, image.isDownloaded());
- values.put(KEY_FILE_URL, image.getFile_url());
- if (image.getId() == 0) {
- image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values));
- } else {
- db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?",
- new String[]{String.valueOf(image.getId())});
- }
-
- final FeedComponent owner = image.getOwner();
- if (owner != null && owner.getId() != 0) {
- values.clear();
- values.put(KEY_IMAGE, image.getId());
- if (owner instanceof Feed) {
- db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())});
- }
- }
- db.setTransactionSuccessful();
- db.endTransaction();
- return image.getId();
- }
-
- /**
- * Inserts or updates an image entry
- *
- * @return the id of the entry
- */
- public long setMedia(FeedMedia media) {
- ContentValues values = new ContentValues();
- values.put(KEY_DURATION, media.getDuration());
- values.put(KEY_POSITION, media.getPosition());
- values.put(KEY_SIZE, media.getSize());
- values.put(KEY_MIME_TYPE, media.getMime_type());
- values.put(KEY_DOWNLOAD_URL, media.getDownload_url());
- values.put(KEY_DOWNLOADED, media.isDownloaded());
- values.put(KEY_FILE_URL, media.getFile_url());
-
- if (media.getPlaybackCompletionDate() != null) {
- values.put(KEY_PLAYBACK_COMPLETION_DATE, media
- .getPlaybackCompletionDate().getTime());
- } else {
- values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
- }
- if (media.getItem() != null) {
- values.put(KEY_FEEDITEM, media.getItem().getId());
- }
- if (media.getId() == 0) {
- media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values));
- } else {
- db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
- new String[]{String.valueOf(media.getId())});
- }
- return media.getId();
- }
-
- public void setFeedMediaPlaybackInformation(FeedMedia media) {
- if (media.getId() != 0) {
- ContentValues values = new ContentValues();
- values.put(KEY_POSITION, media.getPosition());
- values.put(KEY_DURATION, media.getDuration());
- values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
- db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
- new String[]{String.valueOf(media.getId())});
- } else {
- Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0");
- }
- }
-
- public void setFeedMediaPlaybackCompletionDate(FeedMedia media) {
- if (media.getId() != 0) {
- ContentValues values = new ContentValues();
- values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime());
- values.put(KEY_PLAYED_DURATION, media.getPlayedDuration());
- db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
- new String[]{String.valueOf(media.getId())});
- } else {
- Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0");
- }
- }
-
- /**
- * Insert all FeedItems of a feed and the feed object itself in a single
- * transaction
- */
- public void setCompleteFeed(Feed... feeds) {
- db.beginTransaction();
- for (Feed feed : feeds) {
- setFeed(feed);
- if (feed.getItems() != null) {
- for (FeedItem item : feed.getItems()) {
- setFeedItem(item, false);
- }
- }
- if (feed.getPreferences() != null) {
- setFeedPreferences(feed.getPreferences());
- }
- }
- db.setTransactionSuccessful();
- db.endTransaction();
- }
-
- /**
- * Update the flattr status of a feed
- */
- public void setFeedFlattrStatus(Feed feed) {
- ContentValues values = new ContentValues();
- values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong());
- db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())});
- }
-
- /**
- * Get all feeds in the flattr queue.
- */
- public Cursor getFeedsInFlattrQueueCursor() {
- return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?",
- new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null);
- }
-
- /**
- * Get all feed items in the flattr queue.
- */
- public Cursor getFeedItemsInFlattrQueueCursor() {
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?",
- new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null);
- }
-
- /**
- * Counts feeds and feed items in the flattr queue
- */
- public int getFlattrQueueSize() {
- int res = 0;
- Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s",
- TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null);
- if (c.moveToFirst()) {
- res = c.getInt(0);
- c.close();
- } else {
- Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds");
- }
- c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s",
- TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null);
- if (c.moveToFirst()) {
- res += c.getInt(0);
- c.close();
- } else {
- Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items");
- }
-
- return res;
- }
-
- /**
- * Updates the download URL of a Feed.
- */
- public void setFeedDownloadUrl(String original, String updated) {
- ContentValues values = new ContentValues();
- values.put(KEY_DOWNLOAD_URL, updated);
- db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original});
- }
-
- public void setFeedItemlist(List<FeedItem> items) {
- db.beginTransaction();
- for (FeedItem item : items) {
- setFeedItem(item, true);
- }
- db.setTransactionSuccessful();
- db.endTransaction();
- }
-
- public long setSingleFeedItem(FeedItem item) {
- db.beginTransaction();
- long result = setFeedItem(item, true);
- db.setTransactionSuccessful();
- db.endTransaction();
- return result;
- }
-
- /**
- * Update the flattr status of a FeedItem
- */
- public void setFeedItemFlattrStatus(FeedItem feedItem) {
- ContentValues values = new ContentValues();
- values.put(KEY_FLATTR_STATUS, feedItem.getFlattrStatus().toLong());
- db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(feedItem.getId())});
- }
-
- /**
- * Update the flattr status of a feed or feed item specified by its payment link
- * and the new flattr status to use
- */
- public void setItemFlattrStatus(String url, FlattrStatus status) {
- //Log.d(TAG, "setItemFlattrStatus(" + url + ") = " + status.toString());
- ContentValues values = new ContentValues();
- values.put(KEY_FLATTR_STATUS, status.toLong());
-
- // regexps in sqlite would be neat!
- String[] query_urls = new String[]{
- "*" + url + "&*",
- "*" + url + "%2F&*",
- "*" + url + "",
- "*" + url + "%2F"
- };
-
- if (db.update(TABLE_NAME_FEEDS, values,
- KEY_PAYMENT_LINK + " GLOB ?"
- + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
- + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
- + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls
- ) > 0) {
- Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in Feeds table");
- return;
- }
- if (db.update(TABLE_NAME_FEED_ITEMS, values,
- KEY_PAYMENT_LINK + " GLOB ?"
- + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
- + " OR " + KEY_PAYMENT_LINK + " GLOB ?"
- + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls
- ) > 0) {
- Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in FeedsItems table");
- }
- }
-
- /**
- * Reset flattr status to unflattrd for all items
- */
- public void clearAllFlattrStatus() {
- ContentValues values = new ContentValues();
- values.put(KEY_FLATTR_STATUS, 0);
- db.update(TABLE_NAME_FEEDS, values, null, null);
- db.update(TABLE_NAME_FEED_ITEMS, values, null, null);
- }
-
- /**
- * Inserts or updates a feeditem entry
- *
- * @param item The FeedItem
- * @param saveFeed true if the Feed of the item should also be saved. This should be set to
- * false if the method is executed on a list of FeedItems of the same Feed.
- * @return the id of the entry
- */
- private long setFeedItem(FeedItem item, boolean saveFeed) {
- ContentValues values = new ContentValues();
- values.put(KEY_TITLE, item.getTitle());
- values.put(KEY_LINK, item.getLink());
- if (item.getDescription() != null) {
- values.put(KEY_DESCRIPTION, item.getDescription());
- }
- if (item.getContentEncoded() != null) {
- values.put(KEY_CONTENT_ENCODED, item.getContentEncoded());
- }
- values.put(KEY_PUBDATE, item.getPubDate().getTime());
- values.put(KEY_PAYMENT_LINK, item.getPaymentLink());
- if (saveFeed && item.getFeed() != null) {
- setFeed(item.getFeed());
- }
- values.put(KEY_FEED, item.getFeed().getId());
- values.put(KEY_READ, item.isRead());
- values.put(KEY_HAS_CHAPTERS, item.getChapters() != null);
- values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
- values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong());
- if (item.hasItemImage()) {
- if (item.getImage().getId() == 0) {
- setImage(item.getImage());
- }
- values.put(KEY_IMAGE, item.getImage().getId());
- }
-
- if (item.getId() == 0) {
- item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));
- } else {
- db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?",
- new String[]{String.valueOf(item.getId())});
- }
- if (item.getMedia() != null) {
- setMedia(item.getMedia());
- }
- if (item.getChapters() != null) {
- setChapters(item);
- }
- return item.getId();
- }
-
- public void setFeedItemRead(boolean read, long itemId, long mediaId,
- boolean resetMediaPosition) {
- db.beginTransaction();
- ContentValues values = new ContentValues();
-
- values.put(KEY_READ, read);
- db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)});
-
- if (resetMediaPosition) {
- values.clear();
- values.put(KEY_POSITION, 0);
- db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(mediaId)});
- }
-
- db.setTransactionSuccessful();
- db.endTransaction();
- }
-
- public void setFeedItemRead(boolean read, long... itemIds) {
- db.beginTransaction();
- ContentValues values = new ContentValues();
- for (long id : itemIds) {
- values.clear();
- values.put(KEY_READ, read);
- db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(id)});
- }
- db.setTransactionSuccessful();
- db.endTransaction();
- }
-
- public void setChapters(FeedItem item) {
- ContentValues values = new ContentValues();
- for (Chapter chapter : item.getChapters()) {
- values.put(KEY_TITLE, chapter.getTitle());
- values.put(KEY_START, chapter.getStart());
- values.put(KEY_FEEDITEM, item.getId());
- values.put(KEY_LINK, chapter.getLink());
- values.put(KEY_CHAPTER_TYPE, chapter.getChapterType());
- if (chapter.getId() == 0) {
- chapter.setId(db
- .insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
- } else {
- db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?",
- new String[]{String.valueOf(chapter.getId())});
- }
- }
- }
-
- /**
- * Inserts or updates a download status.
- */
- public long setDownloadStatus(DownloadStatus status) {
- ContentValues values = new ContentValues();
- values.put(KEY_FEEDFILE, status.getFeedfileId());
- values.put(KEY_FEEDFILETYPE, status.getFeedfileType());
- values.put(KEY_REASON, status.getReason().getCode());
- values.put(KEY_SUCCESSFUL, status.isSuccessful());
- values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime());
- values.put(KEY_REASON_DETAILED, status.getReasonDetailed());
- values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle());
- if (status.getId() == 0) {
- status.setId(db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values));
- } else {
- db.update(TABLE_NAME_DOWNLOAD_LOG, values, KEY_ID + "=?",
- new String[]{String.valueOf(status.getId())});
- }
- return status.getId();
- }
-
- public long getDownloadLogSize() {
- final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_DOWNLOAD_LOG);
- Cursor result = db.rawQuery(query, null);
- long count = 0;
- if (result.moveToFirst()) {
- count = result.getLong(0);
- }
- result.close();
- return count;
- }
-
- public void removeDownloadLogItems(long count) {
- if (count > 0) {
- final String sql = String.format("DELETE FROM %s WHERE %s in (SELECT %s from %s ORDER BY %s ASC LIMIT %d)",
- TABLE_NAME_DOWNLOAD_LOG, KEY_ID, KEY_ID, TABLE_NAME_DOWNLOAD_LOG, KEY_COMPLETION_DATE, count);
- db.execSQL(sql, null);
- }
- }
-
- public void setQueue(List<FeedItem> queue) {
- ContentValues values = new ContentValues();
- db.beginTransaction();
- db.delete(TABLE_NAME_QUEUE, null, null);
- for (int i = 0; i < queue.size(); i++) {
- FeedItem item = queue.get(i);
- values.put(KEY_ID, i);
- values.put(KEY_FEEDITEM, item.getId());
- values.put(KEY_FEED, item.getFeed().getId());
- db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values,
- SQLiteDatabase.CONFLICT_REPLACE);
- }
- db.setTransactionSuccessful();
- db.endTransaction();
- }
-
- public void clearQueue() {
- db.delete(TABLE_NAME_QUEUE, null, null);
- }
-
- public void removeFeedMedia(FeedMedia media) {
- db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?",
- new String[]{String.valueOf(media.getId())});
- }
-
- public void removeChaptersOfItem(FeedItem item) {
- db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?",
- new String[]{String.valueOf(item.getId())});
- }
-
- public void removeFeedImage(FeedImage image) {
- db.delete(TABLE_NAME_FEED_IMAGES, KEY_ID + "=?",
- new String[]{String.valueOf(image.getId())});
- }
-
- /**
- * Remove a FeedItem and its FeedMedia entry.
- */
- public void removeFeedItem(FeedItem item) {
- if (item.getMedia() != null) {
- removeFeedMedia(item.getMedia());
- }
- if (item.getChapters() != null) {
- removeChaptersOfItem(item);
- }
- if (item.hasItemImage()) {
- removeFeedImage(item.getImage());
- }
- db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?",
- new String[]{String.valueOf(item.getId())});
- }
-
- /**
- * Remove a feed with all its FeedItems and Media entries.
- */
- public void removeFeed(Feed feed) {
- db.beginTransaction();
- if (feed.getImage() != null) {
- removeFeedImage(feed.getImage());
- }
- if (feed.getItems() != null) {
- for (FeedItem item : feed.getItems()) {
- removeFeedItem(item);
- }
- }
-
- db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?",
- new String[]{String.valueOf(feed.getId())});
- db.setTransactionSuccessful();
- db.endTransaction();
- }
-
- public void removeDownloadStatus(DownloadStatus remove) {
- db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?",
- new String[]{String.valueOf(remove.getId())});
- }
-
- public void clearPlaybackHistory() {
- ContentValues values = new ContentValues();
- values.put(KEY_PLAYBACK_COMPLETION_DATE, 0);
- db.update(TABLE_NAME_FEED_MEDIA, values, null, null);
- }
-
- /**
- * Get all Feeds from the Feed Table.
- *
- * @return The cursor of the query
- */
- public final Cursor getAllFeedsCursor() {
- Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null,
- KEY_TITLE + " COLLATE NOCASE ASC");
- return c;
- }
-
- public final Cursor getFeedCursorDownloadUrls() {
- return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null);
- }
-
- public final Cursor getExpiredFeedsCursor(long expirationTime) {
- Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_LASTUPDATE + " < " + String.valueOf(System.currentTimeMillis() - expirationTime),
- null, null, null,
- null);
- return c;
- }
-
- /**
- * Returns a cursor with all FeedItems of a Feed. Uses FEEDITEM_SEL_FI_SMALL
- *
- * @param feed The feed you want to get the FeedItems from.
- * @return The cursor of the query
- */
- public final Cursor getAllItemsOfFeedCursor(final Feed feed) {
- return getAllItemsOfFeedCursor(feed.getId());
- }
-
- public final Cursor getAllItemsOfFeedCursor(final long feedId) {
- Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
- + "=?", new String[]{String.valueOf(feedId)}, null, null,
- null
- );
- return c;
- }
-
- /**
- * Return a cursor with the SEL_FI_EXTRA selection of a single feeditem.
- */
- public final Cursor getExtraInformationOfItem(final FeedItem item) {
- Cursor c = db
- .query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_ID + "=?",
- new String[]{String.valueOf(item.getId())}, null,
- null, null);
- return c;
- }
-
- /**
- * Returns a cursor for a DB query in the FeedMedia table for a given ID.
- *
- * @param item The item you want to get the FeedMedia from
- * @return The cursor of the query
- */
- public final Cursor getFeedMediaOfItemCursor(final FeedItem item) {
- Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?",
- new String[]{String.valueOf(item.getMedia().getId())}, null,
- null, null);
- return c;
- }
-
- /**
- * Returns a cursor for a DB query in the FeedImages table for a given ID.
- *
- * @param id ID of the FeedImage
- * @return The cursor of the query
- */
- public final Cursor getImageCursor(final long id) {
- Cursor c = db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + "=?",
- new String[]{String.valueOf(id)}, null, null, null);
- return c;
- }
-
- public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) {
- Cursor c = db.query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_FEEDITEM
- + "=?", new String[]{String.valueOf(item.getId())}, null,
- null, null
- );
- return c;
- }
-
- public final Cursor getDownloadLogCursor(final int limit) {
- Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null,
- null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit);
- return c;
- }
-
- /**
- * Returns a cursor which contains all feed items in the queue. The returned
- * cursor uses the FEEDITEM_SEL_FI_SMALL selection.
- */
- public final Cursor getQueueCursor() {
- Object[] args = (Object[]) new String[]{
- SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID,
- TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE,
- TABLE_NAME_FEED_ITEMS + "." + KEY_ID,
- TABLE_NAME_QUEUE + "." + KEY_FEEDITEM,
- TABLE_NAME_QUEUE + "." + KEY_ID};
- String query = String.format(
- "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args);
- Cursor c = db.rawQuery(query, null);
- /*
- * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
- * "INNER JOIN ? ON ?=?", new String[] { TABLE_NAME_QUEUE,
- * TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." +
- * KEY_FEEDITEM }, null, null, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM);
- */
- return c;
- }
-
- public Cursor getQueueIDCursor() {
- Cursor c = db.query(TABLE_NAME_QUEUE, new String[]{KEY_FEEDITEM}, null, null, null, null, KEY_ID + " ASC", null);
- return c;
- }
-
- /**
- * Returns a cursor which contains all feed items in the unread items list.
- * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection.
- */
- public final Cursor getUnreadItemsCursor() {
- Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_READ
- + "=0", null, null, null, KEY_PUBDATE + " DESC");
- return c;
- }
-
- public final Cursor getUnreadItemIdsCursor() {
- Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID},
- KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC");
- return c;
-
- }
-
- public final Cursor getRecentlyPublishedItemsCursor(int limit) {
- Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, null, null, null, null, KEY_PUBDATE + " DESC LIMIT " + limit);
- return c;
- }
-
- public Cursor getDownloadedItemsCursor() {
- final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
- + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "="
- + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE "
- + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0";
- Cursor c = db.rawQuery(query, null);
- return c;
- }
-
- /**
- * Returns a cursor which contains feed media objects with a playback
- * completion date in ascending order.
- *
- * @param limit The maximum row count of the returned cursor. Must be an
- * integer >= 0.
- * @throws IllegalArgumentException if limit < 0
- */
- public final Cursor getCompletedMediaCursor(int limit) {
- Validate.isTrue(limit >= 0, "Limit must be >= 0");
-
- Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null,
- KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null,
- null, null);
- return c;
- }
-
- public final Cursor getSingleFeedMediaCursor(long id) {
- return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null);
- }
-
- public final Cursor getFeedMediaCursorByItemID(String... mediaIds) {
- int length = mediaIds.length;
- if (length > IN_OPERATOR_MAXIMUM) {
- Log.w(TAG, "Length of id array is larger than "
- + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors");
- int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1;
- Cursor[] cursors = new Cursor[numCursors];
- for (int i = 0; i < numCursors; i++) {
- int neededLength = 0;
- String[] parts = null;
- final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM;
-
- if (elementsLeft >= IN_OPERATOR_MAXIMUM) {
- neededLength = IN_OPERATOR_MAXIMUM;
- parts = Arrays.copyOfRange(mediaIds, i
- * IN_OPERATOR_MAXIMUM, (i + 1)
- * IN_OPERATOR_MAXIMUM);
- } else {
- neededLength = elementsLeft;
- parts = Arrays.copyOfRange(mediaIds, i
- * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM)
- + neededLength);
- }
-
- cursors[i] = db.rawQuery("SELECT * FROM "
- + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_FEEDITEM + " IN "
- + buildInOperator(neededLength), parts);
- }
- return new MergeCursor(cursors);
- } else {
- return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN "
- + buildInOperator(length), mediaIds, null, null, null);
- }
- }
-
- /**
- * Builds an IN-operator argument depending on the number of items.
- */
- private String buildInOperator(int size) {
- if (size == 1) {
- return "(?)";
- }
- StringBuffer buffer = new StringBuffer("(");
- for (int i = 0; i < size - 1; i++) {
- buffer.append("?,");
- }
- buffer.append("?)");
- return buffer.toString();
- }
-
- public final Cursor getFeedCursor(final long id) {
- Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_ID + "=" + id, null,
- null, null, null);
- return c;
- }
-
- public final Cursor getFeedItemCursor(final String... ids) {
- if (ids.length > IN_OPERATOR_MAXIMUM) {
- throw new IllegalArgumentException(
- "number of IDs must not be larger than "
- + IN_OPERATOR_MAXIMUM
- );
- }
-
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_ID + " IN "
- + buildInOperator(ids.length), ids, null, null, null);
-
- }
-
- public int getQueueSize() {
- final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
- Cursor c = db.rawQuery(query, null);
- int result = 0;
- if (c.moveToFirst()) {
- result = c.getInt(0);
- }
- c.close();
- return result;
- }
-
- public final int getNumberOfUnreadItems() {
- final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS +
- " WHERE " + KEY_READ + " = 0";
- Cursor c = db.rawQuery(query, null);
- int result = 0;
- if (c.moveToFirst()) {
- result = c.getInt(0);
- }
- c.close();
- return result;
- }
-
- public final int getNumberOfDownloadedEpisodes() {
- final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA +
- " WHERE " + KEY_DOWNLOADED + " > 0";
-
- Cursor c = db.rawQuery(query, null);
- int result = 0;
- if (c.moveToFirst()) {
- result = c.getInt(0);
- }
- c.close();
- return result;
- }
-
- /**
- * Uses DatabaseUtils to escape a search query and removes ' at the
- * beginning and the end of the string returned by the escape method.
- */
- private String prepareSearchQuery(String query) {
- StringBuilder builder = new StringBuilder();
- DatabaseUtils.appendEscapedSQLString(builder, query);
- builder.deleteCharAt(0);
- builder.deleteCharAt(builder.length() - 1);
- return builder.toString();
- }
-
- /**
- * Searches for the given query in the description of all items or the items
- * of a specified feed.
- *
- * @return A cursor with all search results in SEL_FI_EXTRA selection.
- */
- public Cursor searchItemDescriptions(long feedID, String query) {
- if (feedID != 0) {
- // search items in specific feed
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
- + "=? AND " + KEY_DESCRIPTION + " LIKE '%"
- + prepareSearchQuery(query) + "%'",
- new String[]{String.valueOf(feedID)}, null, null,
- null
- );
- } else {
- // search through all items
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
- KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query)
- + "%'", null, null, null, null
- );
- }
- }
-
- /**
- * Searches for the given query in the content-encoded field of all items or
- * the items of a specified feed.
- *
- * @return A cursor with all search results in SEL_FI_EXTRA selection.
- */
- public Cursor searchItemContentEncoded(long feedID, String query) {
- if (feedID != 0) {
- // search items in specific feed
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
- + "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%"
- + prepareSearchQuery(query) + "%'",
- new String[]{String.valueOf(feedID)}, null, null,
- null
- );
- } else {
- // search through all items
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
- KEY_CONTENT_ENCODED + " LIKE '%"
- + prepareSearchQuery(query) + "%'", null, null,
- null, null
- );
- }
- }
-
- public Cursor searchItemTitles(long feedID, String query) {
- if (feedID != 0) {
- // search items in specific feed
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
- + "=? AND " + KEY_TITLE + " LIKE '%"
- + prepareSearchQuery(query) + "%'",
- new String[]{String.valueOf(feedID)}, null, null,
- null
- );
- } else {
- // search through all items
- return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
- KEY_TITLE + " LIKE '%"
- + prepareSearchQuery(query) + "%'", null, null,
- null, null
- );
- }
- }
-
- public Cursor searchItemChapters(long feedID, String searchQuery) {
- final String query;
- if (feedID != 0) {
- query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " +
- TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" +
- TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" +
- feedID + " AND " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%"
- + prepareSearchQuery(searchQuery) + "%'";
- } else {
- query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " +
- TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" +
- TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%"
- + prepareSearchQuery(searchQuery) + "%'";
- }
- return db.rawQuery(query, null);
- }
-
-
- public static final int IDX_FEEDSTATISTICS_FEED = 0;
- public static final int IDX_FEEDSTATISTICS_NUM_ITEMS = 1;
- public static final int IDX_FEEDSTATISTICS_NEW_ITEMS = 2;
- public static final int IDX_FEEDSTATISTICS_LATEST_EPISODE = 3;
- public static final int IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES = 4;
-
- /**
- * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result
- * is sorted by the title of the feed.
- */
- private static final String FEED_STATISTICS_QUERY = "SELECT Feeds.id, num_items, new_items, latest_episode, in_progress FROM " +
- " Feeds LEFT JOIN " +
- "(SELECT feed,count(*) AS num_items," +
- " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," +
- " MAX(pubDate) AS latest_episode," +
- " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," +
- " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " +
- " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" +
- " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;";
-
- public Cursor getFeedStatisticsCursor() {
- return db.rawQuery(FEED_STATISTICS_QUERY, null);
- }
-
- /**
- * Helper class for opening the Antennapod database.
- */
- private static class PodDBHelper extends SQLiteOpenHelper {
- /**
- * Constructor.
- *
- * @param context Context to use
- * @param name Name of the database
- * @param factory to use for creating cursor objects
- * @param version number of the database
- */
- public PodDBHelper(final Context context, final String name,
- final CursorFactory factory, final int version) {
- super(context, name, factory, version);
- }
-
- @Override
- public void onCreate(final SQLiteDatabase db) {
- db.execSQL(CREATE_TABLE_FEEDS);
- db.execSQL(CREATE_TABLE_FEED_ITEMS);
- db.execSQL(CREATE_TABLE_FEED_IMAGES);
- db.execSQL(CREATE_TABLE_FEED_MEDIA);
- db.execSQL(CREATE_TABLE_DOWNLOAD_LOG);
- db.execSQL(CREATE_TABLE_QUEUE);
- db.execSQL(CREATE_TABLE_SIMPLECHAPTERS);
- }
-
- @Override
- public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
- final int newVersion) {
- Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to "
- + newVersion + ".");
- if (oldVersion <= 1) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN "
- + KEY_TYPE + " TEXT");
- }
- if (oldVersion <= 2) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS
- + " ADD COLUMN " + KEY_LINK + " TEXT");
- }
- if (oldVersion <= 3) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS
- + " ADD COLUMN " + KEY_ITEM_IDENTIFIER + " TEXT");
- }
- if (oldVersion <= 4) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN "
- + KEY_FEED_IDENTIFIER + " TEXT");
- }
- if (oldVersion <= 5) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG
- + " ADD COLUMN " + KEY_REASON_DETAILED + " TEXT");
- db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG
- + " ADD COLUMN " + KEY_DOWNLOADSTATUS_TITLE + " TEXT");
- }
- if (oldVersion <= 6) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS
- + " ADD COLUMN " + KEY_CHAPTER_TYPE + " INTEGER");
- }
- if (oldVersion <= 7) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA
- + " ADD COLUMN " + 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 " + TABLE_NAME_FEED_MEDIA
- + " ADD COLUMN " + KEY_FEEDITEM
- + " INTEGER");
- Cursor feeditemCursor = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID, KEY_MEDIA}, "? > 0", new String[]{KEY_MEDIA}, null, null, null);
- if (feeditemCursor.moveToFirst()) {
- db.beginTransaction();
- ContentValues contentValues = new ContentValues();
- do {
- long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION);
- contentValues.put(KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION));
- db.update(TABLE_NAME_FEED_MEDIA, contentValues, 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 " + TABLE_NAME_FEEDS
- + " ADD COLUMN " + KEY_AUTO_DOWNLOAD
- + " INTEGER DEFAULT 1");
- }
- if (oldVersion <= 10) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS
- + " ADD COLUMN " + KEY_FLATTR_STATUS
- + " INTEGER");
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS
- + " ADD COLUMN " + KEY_FLATTR_STATUS
- + " INTEGER");
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA
- + " ADD COLUMN " + KEY_PLAYED_DURATION
- + " INTEGER");
- }
- if (oldVersion <= 11) {
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS
- + " ADD COLUMN " + KEY_USERNAME
- + " TEXT");
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS
- + " ADD COLUMN " + KEY_PASSWORD
- + " TEXT");
- db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS
- + " ADD COLUMN " + KEY_IMAGE
- + " INTEGER");
- }
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/syndication/handler/FeedHandler.java b/src/de/danoeh/antennapod/syndication/handler/FeedHandler.java
deleted file mode 100644
index aafa1c209..000000000
--- a/src/de/danoeh/antennapod/syndication/handler/FeedHandler.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.danoeh.antennapod.syndication.handler;
-
-import de.danoeh.antennapod.feed.Feed;
-import org.apache.commons.io.input.XmlStreamReader;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-import java.io.File;
-import java.io.IOException;
-import java.io.Reader;
-
-public class FeedHandler {
-
- public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException,
- ParserConfigurationException, UnsupportedFeedtypeException {
- TypeGetter tg = new TypeGetter();
- TypeGetter.Type type = tg.getType(feed);
- SyndHandler handler = new SyndHandler(feed, type);
-
- SAXParserFactory factory = SAXParserFactory.newInstance();
- factory.setNamespaceAware(true);
- SAXParser saxParser = factory.newSAXParser();
- File file = new File(feed.getFile_url());
- Reader inputStreamReader = new XmlStreamReader(file);
- InputSource inputSource = new InputSource(inputStreamReader);
-
- saxParser.parse(inputSource, handler);
- inputStreamReader.close();
- return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls);
- }
-}
diff --git a/src/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java b/src/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java
deleted file mode 100644
index 41aa29b52..000000000
--- a/src/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.danoeh.antennapod.syndication.handler;
-
-import de.danoeh.antennapod.feed.Feed;
-
-import java.util.Map;
-
-/**
- * Container for results returned by the Feed parser
- */
-public class FeedHandlerResult {
-
- public Feed feed;
- public Map<String, String> alternateFeedUrls;
-
- public FeedHandlerResult(Feed feed, Map<String, String> alternateFeedUrls) {
- this.feed = feed;
- this.alternateFeedUrls = alternateFeedUrls;
- }
-}
diff --git a/src/de/danoeh/antennapod/syndication/handler/HandlerState.java b/src/de/danoeh/antennapod/syndication/handler/HandlerState.java
deleted file mode 100644
index 17f84724f..000000000
--- a/src/de/danoeh/antennapod/syndication/handler/HandlerState.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package de.danoeh.antennapod.syndication.handler;
-
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.syndication.namespace.Namespace;
-import de.danoeh.antennapod.syndication.namespace.SyndElement;
-
-import java.util.*;
-
-/**
- * Contains all relevant information to describe the current state of a
- * SyndHandler.
- */
-public class HandlerState {
-
- /**
- * Feed that the Handler is currently processing.
- */
- protected Feed feed;
- /**
- * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the
- * URL of the feed, the value is the title
- */
- protected Map<String, String> alternateUrls;
- protected ArrayList<FeedItem> items;
- protected FeedItem currentItem;
- protected Stack<SyndElement> tagstack;
- /**
- * Namespaces that have been defined so far.
- */
- protected HashMap<String, Namespace> namespaces;
- protected Stack<Namespace> defaultNamespaces;
- /**
- * Buffer for saving characters.
- */
- protected StringBuffer contentBuf;
-
- public HandlerState(Feed feed) {
- this.feed = feed;
- alternateUrls = new LinkedHashMap<String, String>();
- items = new ArrayList<FeedItem>();
- tagstack = new Stack<SyndElement>();
- namespaces = new HashMap<String, Namespace>();
- defaultNamespaces = new Stack<Namespace>();
- }
-
- public Feed getFeed() {
- return feed;
- }
-
- public ArrayList<FeedItem> getItems() {
- return items;
- }
-
- public FeedItem getCurrentItem() {
- return currentItem;
- }
-
- public Stack<SyndElement> getTagstack() {
- return tagstack;
- }
-
- public void setFeed(Feed feed) {
- this.feed = feed;
- }
-
- public void setCurrentItem(FeedItem currentItem) {
- this.currentItem = currentItem;
- }
-
- /**
- * Returns the SyndElement that comes after the top element of the tagstack.
- */
- public SyndElement getSecondTag() {
- SyndElement top = tagstack.pop();
- SyndElement second = tagstack.peek();
- tagstack.push(top);
- return second;
- }
-
- public SyndElement getThirdTag() {
- SyndElement top = tagstack.pop();
- SyndElement second = tagstack.pop();
- SyndElement third = tagstack.peek();
- tagstack.push(second);
- tagstack.push(top);
- return third;
- }
-
- public StringBuffer getContentBuf() {
- return contentBuf;
- }
-
- public void addAlternateFeedUrl(String title, String url) {
- alternateUrls.put(url, title);
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java b/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java
deleted file mode 100644
index 15dc94d65..000000000
--- a/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package de.danoeh.antennapod.syndication.handler;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.syndication.namespace.*;
-import de.danoeh.antennapod.syndication.namespace.atom.NSAtom;
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-/** Superclass for all SAX Handlers which process Syndication formats */
-public class SyndHandler extends DefaultHandler {
- private static final String TAG = "SyndHandler";
- private static final String DEFAULT_PREFIX = "";
- protected HandlerState state;
-
- public SyndHandler(Feed feed, TypeGetter.Type type) {
- state = new HandlerState(feed);
- if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) {
- state.defaultNamespaces.push(new NSRSS20());
- }
- }
-
- @Override
- public void startElement(String uri, String localName, String qName,
- Attributes attributes) throws SAXException {
- state.contentBuf = new StringBuffer();
- Namespace handler = getHandlingNamespace(uri, qName);
- if (handler != null) {
- SyndElement element = handler.handleElementStart(localName, state,
- attributes);
- state.tagstack.push(element);
-
- }
- }
-
- @Override
- public void characters(char[] ch, int start, int length)
- throws SAXException {
- if (!state.tagstack.empty()) {
- if (state.getTagstack().size() >= 2) {
- if (state.contentBuf != null) {
- state.contentBuf.append(ch, start, length);
- }
- }
- }
- }
-
- @Override
- public void endElement(String uri, String localName, String qName)
- throws SAXException {
- Namespace handler = getHandlingNamespace(uri, qName);
- if (handler != null) {
- handler.handleElementEnd(localName, state);
- state.tagstack.pop();
-
- }
- state.contentBuf = null;
-
- }
-
- @Override
- public void endPrefixMapping(String prefix) throws SAXException {
- if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) {
- state.defaultNamespaces.pop();
- }
- }
-
- @Override
- public void startPrefixMapping(String prefix, String uri)
- throws SAXException {
- // Find the right namespace
- if (!state.namespaces.containsKey(uri)) {
- if (uri.equals(NSAtom.NSURI)) {
- if (prefix.equals(DEFAULT_PREFIX)) {
- state.defaultNamespaces.push(new NSAtom());
- } else if (prefix.equals(NSAtom.NSTAG)) {
- state.namespaces.put(uri, new NSAtom());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized Atom namespace");
- }
- } else if (uri.equals(NSContent.NSURI)
- && prefix.equals(NSContent.NSTAG)) {
- state.namespaces.put(uri, new NSContent());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized Content namespace");
- } else if (uri.equals(NSITunes.NSURI)
- && prefix.equals(NSITunes.NSTAG)) {
- state.namespaces.put(uri, new NSITunes());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized ITunes namespace");
- } else if (uri.equals(NSSimpleChapters.NSURI)
- && prefix.matches(NSSimpleChapters.NSTAG)) {
- state.namespaces.put(uri, new NSSimpleChapters());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized SimpleChapters namespace");
- } else if (uri.equals(NSMedia.NSURI)
- && prefix.equals(NSMedia.NSTAG)) {
- state.namespaces.put(uri, new NSMedia());
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized media namespace");
- }
- }
- }
-
- private Namespace getHandlingNamespace(String uri, String qName) {
- Namespace handler = state.namespaces.get(uri);
- if (handler == null && !state.defaultNamespaces.empty()
- && !qName.contains(":")) {
- handler = state.defaultNamespaces.peek();
- }
- return handler;
- }
-
- @Override
- public void endDocument() throws SAXException {
- super.endDocument();
- state.getFeed().setItems(state.getItems());
- }
-
- public HandlerState getState() {
- return state;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java
deleted file mode 100644
index 2496e112a..000000000
--- a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package de.danoeh.antennapod.syndication.handler;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Feed;
-import org.apache.commons.io.input.XmlStreamReader;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.Reader;
-
-/** Gets the type of a specific feed by reading the root element. */
-public class TypeGetter {
- private static final String TAG = "TypeGetter";
-
- public enum Type {
- RSS20, RSS091, ATOM, INVALID
- }
-
- private static final String ATOM_ROOT = "feed";
- private static final String RSS_ROOT = "rss";
-
- public Type getType(Feed feed) throws UnsupportedFeedtypeException {
- XmlPullParserFactory factory;
- if (feed.getFile_url() != null) {
- try {
- factory = XmlPullParserFactory.newInstance();
- factory.setNamespaceAware(true);
- XmlPullParser xpp = factory.newPullParser();
- xpp.setInput(createReader(feed));
- int eventType = xpp.getEventType();
-
- while (eventType != XmlPullParser.END_DOCUMENT) {
- if (eventType == XmlPullParser.START_TAG) {
- String tag = xpp.getName();
- if (tag.equals(ATOM_ROOT)) {
- feed.setType(Feed.TYPE_ATOM1);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized type Atom");
- return Type.ATOM;
- } else if (tag.equals(RSS_ROOT)) {
- String strVersion = xpp.getAttributeValue(null,
- "version");
- if (strVersion != null) {
-
- if (strVersion.equals("2.0")) {
- feed.setType(Feed.TYPE_RSS2);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized type RSS 2.0");
- return Type.RSS20;
- } else if (strVersion.equals("0.91")
- || strVersion.equals("0.92")) {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Recognized type RSS 0.91/0.92");
- return Type.RSS091;
- }
- }
- throw new UnsupportedFeedtypeException(Type.INVALID);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Type is invalid");
- throw new UnsupportedFeedtypeException(Type.INVALID, tag);
- }
- } else {
- eventType = xpp.next();
- }
- }
-
- } catch (XmlPullParserException e) {
- e.printStackTrace();
- // XML document might actually be a HTML document -> try to parse as HTML
- String rootElement = null;
- try {
- if (Jsoup.parse(new File(feed.getFile_url()), null) != null) {
- rootElement = "html";
- }
- } catch (IOException e1) {
- e1.printStackTrace();
- } finally {
- throw new UnsupportedFeedtypeException(Type.INVALID, rootElement);
- }
-
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Type is invalid");
- throw new UnsupportedFeedtypeException(Type.INVALID);
- }
-
- private Reader createReader(Feed feed) {
- Reader reader;
- try {
- reader = new XmlStreamReader(new File(feed.getFile_url()));
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return null;
- } catch (IOException e) {
- e.printStackTrace();
- return null;
- }
- return reader;
- }
-}
diff --git a/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java b/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java
deleted file mode 100644
index 605dad2fb..000000000
--- a/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package de.danoeh.antennapod.syndication.handler;
-
-import de.danoeh.antennapod.syndication.handler.TypeGetter.Type;
-
-public class UnsupportedFeedtypeException extends Exception {
- private static final long serialVersionUID = 9105878964928170669L;
- private TypeGetter.Type type;
- private String rootElement;
-
- public UnsupportedFeedtypeException(Type type) {
- super();
- this.type = type;
- }
-
- public UnsupportedFeedtypeException(Type type, String rootElement) {
- this.type = type;
- this.rootElement = rootElement;
- }
-
- public TypeGetter.Type getType() {
- return type;
- }
-
- public String getRootElement() {
- return rootElement;
- }
-
- @Override
- public String getMessage() {
- if (type == TypeGetter.Type.INVALID) {
- return "Invalid type";
- } else {
- return "Type " + type + " not supported";
- }
- }
-
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/NSContent.java b/src/de/danoeh/antennapod/syndication/namespace/NSContent.java
deleted file mode 100644
index 9ad3026be..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/NSContent.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace;
-
-import de.danoeh.antennapod.syndication.handler.HandlerState;
-import org.xml.sax.Attributes;
-
-public class NSContent extends Namespace {
- public static final String NSTAG = "content";
- public static final String NSURI = "http://purl.org/rss/1.0/modules/content/";
-
- private static final String ENCODED = "encoded";
-
- @Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- return new SyndElement(localName, this);
- }
-
- @Override
- public void handleElementEnd(String localName, HandlerState state) {
- if (localName.equals(ENCODED)) {
- state.getCurrentItem().setContentEncoded(state.getContentBuf().toString());
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/NSITunes.java b/src/de/danoeh/antennapod/syndication/namespace/NSITunes.java
deleted file mode 100644
index d8cbe040b..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/NSITunes.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace;
-
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.syndication.handler.HandlerState;
-import org.xml.sax.Attributes;
-
-public class NSITunes extends Namespace {
- public static final String NSTAG = "itunes";
- public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd";
-
- private static final String IMAGE = "image";
- private static final String IMAGE_TITLE = "image";
- private static final String IMAGE_HREF = "href";
-
- private static final String AUTHOR = "author";
-
-
- @Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- if (localName.equals(IMAGE)) {
- FeedImage image = new FeedImage();
- image.setTitle(IMAGE_TITLE);
- image.setDownload_url(attributes.getValue(IMAGE_HREF));
-
- if (state.getCurrentItem() != null) {
- // this is an items image
- image.setTitle(state.getCurrentItem().getTitle() + IMAGE_TITLE);
- state.getCurrentItem().setImage(image);
-
- } else {
- // this is the feed image
- if (state.getFeed().getImage() == null) {
- state.getFeed().setImage(image);
- }
- }
-
- }
-
- return new SyndElement(localName, this);
- }
-
- @Override
- public void handleElementEnd(String localName, HandlerState state) {
- if (localName.equals(AUTHOR)) {
- state.getFeed().setAuthor(state.getContentBuf().toString());
- }
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/NSMedia.java b/src/de/danoeh/antennapod/syndication/namespace/NSMedia.java
deleted file mode 100644
index cc23167c1..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/NSMedia.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.syndication.handler.HandlerState;
-import de.danoeh.antennapod.syndication.util.SyndTypeUtils;
-import org.xml.sax.Attributes;
-
-import java.util.concurrent.TimeUnit;
-
-/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */
-public class NSMedia extends Namespace {
- private static final String TAG = "NSMedia";
-
- public static final String NSTAG = "media";
- public static final String NSURI = "http://search.yahoo.com/mrss/";
-
- private static final String CONTENT = "content";
- private static final String DOWNLOAD_URL = "url";
- private static final String SIZE = "fileSize";
- private static final String MIME_TYPE = "type";
- private static final String DURATION = "duration";
-
- @Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- if (localName.equals(CONTENT)) {
- String url = attributes.getValue(DOWNLOAD_URL);
- String type = attributes.getValue(MIME_TYPE);
- if (state.getCurrentItem().getMedia() == null
- && url != null
- && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils
- .getValidMimeTypeFromUrl(url)) != null))) {
-
- long size = 0;
- try {
- size = Long.parseLong(attributes.getValue(SIZE));
- } catch (NumberFormatException e) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Length attribute could not be parsed.");
- }
-
- int duration = 0;
- try {
- String durationStr = attributes.getValue(DURATION);
- if (durationStr != null) {
- duration = (int) TimeUnit.MILLISECONDS.convert(
- Long.parseLong(durationStr), TimeUnit.SECONDS);
- }
- } catch (NumberFormatException e) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Duration attribute could not be parsed");
- }
-
- state.getCurrentItem().setMedia(
- new FeedMedia(state.getCurrentItem(), url, size, type));
- }
- }
- return new SyndElement(localName, this);
- }
-
- @Override
- public void handleElementEnd(String localName, HandlerState state) {
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java b/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java
deleted file mode 100644
index 9572f87ae..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/NSRSS20.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.syndication.handler.HandlerState;
-import de.danoeh.antennapod.syndication.util.SyndDateUtils;
-import de.danoeh.antennapod.syndication.util.SyndTypeUtils;
-import org.xml.sax.Attributes;
-
-/**
- * SAX-Parser for reading RSS-Feeds
- *
- * @author daniel
- *
- */
-public class NSRSS20 extends Namespace {
- private static final String TAG = "NSRSS20";
- public static final String NSTAG = "rss";
- public static final String NSURI = "";
-
- public final static String CHANNEL = "channel";
- public final static String ITEM = "item";
- public final static String GUID = "guid";
- public final static String TITLE = "title";
- public final static String LINK = "link";
- public final static String DESCR = "description";
- public final static String PUBDATE = "pubDate";
- public final static String ENCLOSURE = "enclosure";
- public final static String IMAGE = "image";
- public final static String URL = "url";
- public final static String LANGUAGE = "language";
-
- public final static String ENC_URL = "url";
- public final static String ENC_LEN = "length";
- public final static String ENC_TYPE = "type";
-
- @Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- if (localName.equals(ITEM)) {
- state.setCurrentItem(new FeedItem());
- state.getItems().add(state.getCurrentItem());
- state.getCurrentItem().setFeed(state.getFeed());
-
- } else if (localName.equals(ENCLOSURE)) {
- String type = attributes.getValue(ENC_TYPE);
- String url = attributes.getValue(ENC_URL);
- if (state.getCurrentItem().getMedia() == null
- && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils
- .getValidMimeTypeFromUrl(url)) != null))) {
-
- long size = 0;
- try {
- size = Long.parseLong(attributes.getValue(ENC_LEN));
- } catch (NumberFormatException e) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Length attribute could not be parsed.");
- }
- state.getCurrentItem().setMedia(
- new FeedMedia(state.getCurrentItem(), url, size, type));
- }
-
- } else if (localName.equals(IMAGE)) {
- if (state.getTagstack().size() >= 1) {
- String parent = state.getTagstack().peek().getName();
- if (parent.equals(CHANNEL)) {
- state.getFeed().setImage(new FeedImage());
- }
- }
- }
- return new SyndElement(localName, this);
- }
-
- @Override
- public void handleElementEnd(String localName, HandlerState state) {
- if (localName.equals(ITEM)) {
- if (state.getCurrentItem() != null) {
- // the title tag is optional in RSS 2.0. The description is used
- // as a
- // title if the item has no title-tag.
- if (state.getCurrentItem().getTitle() == null) {
- state.getCurrentItem().setTitle(
- state.getCurrentItem().getDescription());
- }
- }
- state.setCurrentItem(null);
- } else if (state.getTagstack().size() >= 2
- && state.getContentBuf() != null) {
- String content = state.getContentBuf().toString();
- SyndElement topElement = state.getTagstack().peek();
- String top = topElement.getName();
- SyndElement secondElement = state.getSecondTag();
- String second = secondElement.getName();
- String third = null;
- if (state.getTagstack().size() >= 3) {
- third = state.getThirdTag().getName();
- }
-
- if (top.equals(GUID) && second.equals(ITEM)) {
- // some feed creators include an empty or non-standard guid-element in their feed, which should be ignored
- if (!content.isEmpty()) {
- state.getCurrentItem().setItemIdentifier(content);
- }
- } else if (top.equals(TITLE)) {
- if (second.equals(ITEM)) {
- state.getCurrentItem().setTitle(content);
- } else if (second.equals(CHANNEL)) {
- state.getFeed().setTitle(content);
- } else if (second.equals(IMAGE) && third != null
- && third.equals(CHANNEL)) {
- state.getFeed().getImage().setTitle(content);
- }
- } else if (top.equals(LINK)) {
- if (second.equals(CHANNEL)) {
- state.getFeed().setLink(content);
- } else if (second.equals(ITEM)) {
- state.getCurrentItem().setLink(content);
- }
- } else if (top.equals(PUBDATE) && second.equals(ITEM)) {
- state.getCurrentItem().setPubDate(
- SyndDateUtils.parseRFC822Date(content));
- } else if (top.equals(URL) && second.equals(IMAGE) && third != null
- && third.equals(CHANNEL)) {
- state.getFeed().getImage().setDownload_url(content);
- } else if (localName.equals(DESCR)) {
- if (second.equals(CHANNEL)) {
- state.getFeed().setDescription(content);
- } else if (second.equals(ITEM)) {
- state.getCurrentItem().setDescription(content);
- }
-
- } else if (localName.equals(LANGUAGE)) {
- state.getFeed().setLanguage(content.toLowerCase());
- }
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java b/src/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java
deleted file mode 100644
index b45793b6b..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace;
-
-import android.util.Log;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.SimpleChapter;
-import de.danoeh.antennapod.syndication.handler.HandlerState;
-import de.danoeh.antennapod.syndication.util.SyndDateUtils;
-import org.xml.sax.Attributes;
-
-import java.util.ArrayList;
-
-public class NSSimpleChapters extends Namespace {
- private static final String TAG = "NSSimpleChapters";
-
- public static final String NSTAG = "psc|sc";
- public static final String NSURI = "http://podlove.org/simple-chapters";
-
- public static final String CHAPTERS = "chapters";
- public static final String CHAPTER = "chapter";
- public static final String START = "start";
- public static final String TITLE = "title";
- public static final String HREF = "href";
-
- @Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- if (localName.equals(CHAPTERS)) {
- state.getCurrentItem().setChapters(new ArrayList<Chapter>());
- } else if (localName.equals(CHAPTER)) {
- try {
- state.getCurrentItem()
- .getChapters()
- .add(new SimpleChapter(SyndDateUtils
- .parseTimeString(attributes.getValue(START)),
- attributes.getValue(TITLE), state.getCurrentItem(),
- attributes.getValue(HREF)));
- } catch (NumberFormatException e) {
- if (BuildConfig.DEBUG) Log.w(TAG, "Unable to read chapter", e);
- }
- }
-
- return new SyndElement(localName, this);
- }
-
- @Override
- public void handleElementEnd(String localName, HandlerState state) {
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/Namespace.java b/src/de/danoeh/antennapod/syndication/namespace/Namespace.java
deleted file mode 100644
index 910131feb..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/Namespace.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace;
-
-import de.danoeh.antennapod.syndication.handler.HandlerState;
-import org.xml.sax.Attributes;
-
-
-public abstract class Namespace {
- public static final String NSTAG = null;
- public static final String NSURI = null;
-
- /** Called by a Feedhandler when in startElement and it detects a namespace element
- * @return The SyndElement to push onto the stack
- * */
- public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes);
-
- /** Called by a Feedhandler when in endElement and it detects a namespace element
- * @return true if namespace handled the element, false if it ignored it
- * */
- public abstract void handleElementEnd(String localName, HandlerState state);
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/SyndElement.java b/src/de/danoeh/antennapod/syndication/namespace/SyndElement.java
deleted file mode 100644
index 187312c9e..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/SyndElement.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace;
-
-/** Defines a XML Element that is pushed on the tagstack */
-public class SyndElement {
- protected String name;
- protected Namespace namespace;
-
- public SyndElement(String name, Namespace namespace) {
- this.name = name;
- this.namespace = namespace;
- }
-
- public Namespace getNamespace() {
- return namespace;
- }
-
- public String getName() {
- return name;
- }
-
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java b/src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java
deleted file mode 100644
index 86b80d2ed..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace.atom;
-
-import de.danoeh.antennapod.syndication.namespace.Namespace;
-import de.danoeh.antennapod.syndication.namespace.SyndElement;
-import org.apache.commons.lang3.StringEscapeUtils;
-
-/** Represents Atom Element which contains text (content, title, summary). */
-public class AtomText extends SyndElement {
- public static final String TYPE_TEXT = "text";
- public static final String TYPE_HTML = "html";
- public static final String TYPE_XHTML = "xhtml";
-
- private String type;
- private String content;
-
- public AtomText(String name, Namespace namespace, String type) {
- super(name, namespace);
- this.type = type;
- }
-
- /** Processes the content according to the type and returns it. */
- public String getProcessedContent() {
- if (type == null) {
- return content;
- } else if (type.equals(TYPE_HTML)) {
- return StringEscapeUtils.unescapeHtml4(content);
- } else if (type.equals(TYPE_XHTML)) {
- return content;
- } else { // Handle as text by default
- return content;
- }
- }
-
- public String getContent() {
- return content;
- }
-
- public void setContent(String content) {
- this.content = content;
- }
-
- public String getType() {
- return type;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java b/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
deleted file mode 100644
index 2c8e232ff..000000000
--- a/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
+++ /dev/null
@@ -1,194 +0,0 @@
-package de.danoeh.antennapod.syndication.namespace.atom;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.syndication.handler.HandlerState;
-import de.danoeh.antennapod.syndication.namespace.NSRSS20;
-import de.danoeh.antennapod.syndication.namespace.Namespace;
-import de.danoeh.antennapod.syndication.namespace.SyndElement;
-import de.danoeh.antennapod.syndication.util.SyndDateUtils;
-import de.danoeh.antennapod.syndication.util.SyndTypeUtils;
-import org.xml.sax.Attributes;
-
-public class NSAtom extends Namespace {
- private static final String TAG = "NSAtom";
- public static final String NSTAG = "atom";
- public static final String NSURI = "http://www.w3.org/2005/Atom";
-
- private static final String FEED = "feed";
- private static final String ID = "id";
- private static final String TITLE = "title";
- private static final String ENTRY = "entry";
- private static final String LINK = "link";
- private static final String UPDATED = "updated";
- private static final String AUTHOR = "author";
- private static final String CONTENT = "content";
- private static final String IMAGE = "logo";
- private static final String SUBTITLE = "subtitle";
- private static final String PUBLISHED = "published";
-
- private static final String TEXT_TYPE = "type";
- // Link
- private static final String LINK_HREF = "href";
- private static final String LINK_REL = "rel";
- private static final String LINK_TYPE = "type";
- private static final String LINK_TITLE = "title";
- private static final String LINK_LENGTH = "length";
- // rel-values
- private static final String LINK_REL_ALTERNATE = "alternate";
- private static final String LINK_REL_ENCLOSURE = "enclosure";
- private static final String LINK_REL_PAYMENT = "payment";
- private static final String LINK_REL_RELATED = "related";
- private static final String LINK_REL_SELF = "self";
- // type-values
- private static final String LINK_TYPE_ATOM = "application/atom+xml";
- private static final String LINK_TYPE_HTML = "text/html";
- private static final String LINK_TYPE_XHTML = "application/xml+xhtml";
-
- private static final String LINK_TYPE_RSS = "application/rss+xml";
-
- /**
- * Regexp to test whether an Element is a Text Element.
- */
- private static final String isText = TITLE + "|" + CONTENT + "|" + "|"
- + SUBTITLE;
-
- public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL;
- public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM;
-
- @Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- if (localName.equals(ENTRY)) {
- state.setCurrentItem(new FeedItem());
- state.getItems().add(state.getCurrentItem());
- state.getCurrentItem().setFeed(state.getFeed());
- } else if (localName.matches(isText)) {
- String type = attributes.getValue(TEXT_TYPE);
- return new AtomText(localName, this, type);
- } else if (localName.equals(LINK)) {
- String href = attributes.getValue(LINK_HREF);
- String rel = attributes.getValue(LINK_REL);
- SyndElement parent = state.getTagstack().peek();
- if (parent.getName().matches(isFeedItem)) {
- if (rel == null || rel.equals(LINK_REL_ALTERNATE)) {
- state.getCurrentItem().setLink(href);
- } else if (rel.equals(LINK_REL_ENCLOSURE)) {
- String strSize = attributes.getValue(LINK_LENGTH);
- long size = 0;
- try {
- if (strSize != null) {
- size = Long.parseLong(strSize);
- }
- } catch (NumberFormatException e) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Length attribute could not be parsed.");
- }
- String type = attributes.getValue(LINK_TYPE);
- if (SyndTypeUtils.enclosureTypeValid(type)
- || (type = SyndTypeUtils
- .getValidMimeTypeFromUrl(href)) != null) {
- state.getCurrentItem().setMedia(
- new FeedMedia(state.getCurrentItem(), href,
- size, type)
- );
- }
- } else if (rel.equals(LINK_REL_PAYMENT)) {
- state.getCurrentItem().setPaymentLink(href);
- }
- } else if (parent.getName().matches(isFeed)) {
- if (rel == null || rel.equals(LINK_REL_ALTERNATE)) {
- String type = attributes.getValue(LINK_TYPE);
- /*
- * Use as link if a) no type-attribute is given and
- * feed-object has no link yet b) type of link is
- * LINK_TYPE_HTML or LINK_TYPE_XHTML
- */
- if ((type == null && state.getFeed().getLink() == null)
- || (type != null && (type.equals(LINK_TYPE_HTML) || type.equals(LINK_TYPE_XHTML)))) {
- state.getFeed().setLink(href);
- } else if (type != null && (type.equals(LINK_TYPE_ATOM) || type.equals(LINK_TYPE_RSS))) {
- // treat as podlove alternate feed
- String title = attributes.getValue(LINK_TITLE);
- if (title == null) {
- title = href;
- }
- state.addAlternateFeedUrl(title, href);
- }
- } else if (rel.equals(LINK_REL_PAYMENT)) {
- state.getFeed().setPaymentLink(href);
- }
- }
- }
- return new SyndElement(localName, this);
- }
-
- @Override
- public void handleElementEnd(String localName, HandlerState state) {
- if (localName.equals(ENTRY)) {
- state.setCurrentItem(null);
- }
-
- if (state.getTagstack().size() >= 2) {
- AtomText textElement = null;
- String content;
- if (state.getContentBuf() != null) {
- content = state.getContentBuf().toString();
- } else {
- content = "";
- }
- SyndElement topElement = state.getTagstack().peek();
- String top = topElement.getName();
- SyndElement secondElement = state.getSecondTag();
- String second = secondElement.getName();
-
- if (top.matches(isText)) {
- textElement = (AtomText) topElement;
- textElement.setContent(content);
- }
-
- if (top.equals(ID)) {
- if (second.equals(FEED)) {
- state.getFeed().setFeedIdentifier(content);
- } else if (second.equals(ENTRY)) {
- state.getCurrentItem().setItemIdentifier(content);
- }
- } else if (top.equals(TITLE)) {
-
- if (second.equals(FEED)) {
- state.getFeed().setTitle(textElement.getProcessedContent());
- } else if (second.equals(ENTRY)) {
- state.getCurrentItem().setTitle(
- textElement.getProcessedContent());
- }
- } else if (top.equals(SUBTITLE)) {
- if (second.equals(FEED)) {
- state.getFeed().setDescription(
- textElement.getProcessedContent());
- }
- } else if (top.equals(CONTENT)) {
- if (second.equals(ENTRY)) {
- state.getCurrentItem().setDescription(
- textElement.getProcessedContent());
- }
- } else if (top.equals(UPDATED)) {
- if (second.equals(ENTRY)
- && state.getCurrentItem().getPubDate() == null) {
- state.getCurrentItem().setPubDate(
- SyndDateUtils.parseRFC3339Date(content));
- }
- } else if (top.equals(PUBLISHED)) {
- if (second.equals(ENTRY)) {
- state.getCurrentItem().setPubDate(
- SyndDateUtils.parseRFC3339Date(content));
- }
- } else if (top.equals(IMAGE)) {
- state.getFeed().setImage(new FeedImage(content, null));
- }
-
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java b/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
deleted file mode 100644
index 56687ac2e..000000000
--- a/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
+++ /dev/null
@@ -1,161 +0,0 @@
-package de.danoeh.antennapod.syndication.util;
-
-import android.util.Log;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-/**
- * Parses several date formats.
- */
-public class SyndDateUtils {
- private static final String TAG = "DateUtils";
-
- private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z",};
-
- /**
- * RFC 3339 date format for UTC dates.
- */
- public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'";
-
- /**
- * RFC 3339 date format for localtime dates with offset.
- */
- public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ";
-
- private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() {
- @Override
- protected SimpleDateFormat initialValue() {
- return new SimpleDateFormat(RFC822DATES[0], Locale.US);
- }
-
- };
-
- private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
- @Override
- protected SimpleDateFormat initialValue() {
- return new SimpleDateFormat(RFC3339UTC, Locale.US);
- }
-
- };
-
- public static Date parseRFC822Date(String date) {
- Date result = null;
- if (date.contains("PDT")) {
- date = date.replace("PDT", "PST8PDT");
- }
- if (date.contains(",")) {
- // Remove day of the week
- date = date.substring(date.indexOf(",") + 1).trim();
- }
- SimpleDateFormat format = RFC822Formatter.get();
- for (int i = 0; i < RFC822DATES.length; i++) {
- try {
- format.applyPattern(RFC822DATES[i]);
- result = format.parse(date);
- break;
- } catch (ParseException e) {
- e.printStackTrace();
- }
- }
- if (result == null) {
- Log.e(TAG, "Unable to parse feed date correctly");
- }
-
- return result;
- }
-
- public static Date parseRFC3339Date(String date) {
- Date result = null;
- SimpleDateFormat format = RFC3339Formatter.get();
- boolean isLocal = date.endsWith("Z");
- if (date.contains(".")) {
- // remove secfrac
- int fracIndex = date.indexOf(".");
- String first = date.substring(0, fracIndex);
- String second = null;
- if (isLocal) {
- second = date.substring(date.length() - 1);
- } else {
- if (date.contains("+")) {
- second = date.substring(date.indexOf("+"));
- } else {
- second = date.substring(date.indexOf("-"));
- }
- }
-
- date = first + second;
- }
- if (isLocal) {
- try {
- result = format.parse(date);
- } catch (ParseException e) {
- e.printStackTrace();
- }
- } else {
- format.applyPattern(RFC3339LOCAL);
- // remove last colon
- StringBuffer buf = new StringBuffer(date.length() - 1);
- int colonIdx = date.lastIndexOf(':');
- for (int x = 0; x < date.length(); x++) {
- if (x != colonIdx)
- buf.append(date.charAt(x));
- }
- String bufStr = buf.toString();
- try {
- result = format.parse(bufStr);
- } catch (ParseException e) {
- e.printStackTrace();
- Log.e(TAG, "Unable to parse date");
- } finally {
- format.applyPattern(RFC3339UTC);
- }
-
- }
-
- return result;
-
- }
-
- /**
- * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to
- * milliseconds.
- *
- * @throws java.lang.NumberFormatException if the number segments contain invalid numbers.
- */
- public static long parseTimeString(final String time) {
- String[] parts = time.split(":");
- long result = 0;
- int idx = 0;
- if (parts.length == 3) {
- // string has hours
- result += Integer.valueOf(parts[idx]) * 3600000L;
- idx++;
- }
- result += Integer.valueOf(parts[idx]) * 60000L;
- idx++;
- result += (Float.valueOf(parts[idx])) * 1000L;
- return result;
- }
-
- public static String formatRFC822Date(Date date) {
- SimpleDateFormat format = RFC822Formatter.get();
- return format.format(date);
- }
-
- public static String formatRFC3339Local(Date date) {
- SimpleDateFormat format = RFC3339Formatter.get();
- format.applyPattern(RFC3339LOCAL);
- String result = format.format(date);
- format.applyPattern(RFC3339UTC);
- return result;
- }
-
- public static String formatRFC3339UTC(Date date) {
- SimpleDateFormat format = RFC3339Formatter.get();
- format.applyPattern(RFC3339UTC);
- return format.format(date);
- }
-}
diff --git a/src/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java b/src/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java
deleted file mode 100644
index d0fa3a5fc..000000000
--- a/src/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package de.danoeh.antennapod.syndication.util;
-
-import android.webkit.MimeTypeMap;
-import org.apache.commons.io.FilenameUtils;
-
-/** Utility class for handling MIME-Types of enclosures */
-public class SyndTypeUtils {
-
- private final static String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*"
- + "|" + "application/ogg";
-
- private SyndTypeUtils() {
-
- }
-
- public static boolean enclosureTypeValid(String type) {
- if (type == null) {
- return false;
- } else {
- return type.matches(VALID_MIMETYPE);
- }
- }
-
- /**
- * Should be used if mime-type of enclosure tag is not supported. This
- * method will check if the mime-type of the file extension is supported. If
- * the type is not supported, this method will return null.
- */
- public static String getValidMimeTypeFromUrl(String url) {
- if (url != null) {
- String extension = FilenameUtils.getExtension(url);
- if (extension != null) {
- String type = MimeTypeMap.getSingleton()
- .getMimeTypeFromExtension(extension);
- if (type != null && enclosureTypeValid(type)) {
- return type;
- }
- }
- }
- return null;
- }
-}
diff --git a/src/de/danoeh/antennapod/util/ChapterUtils.java b/src/de/danoeh/antennapod/util/ChapterUtils.java
deleted file mode 100644
index 2d9022eed..000000000
--- a/src/de/danoeh/antennapod/util/ChapterUtils.java
+++ /dev/null
@@ -1,274 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.util.Log;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collections;
-import java.util.List;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.util.comparator.ChapterStartTimeComparator;
-import de.danoeh.antennapod.util.id3reader.ChapterReader;
-import de.danoeh.antennapod.util.id3reader.ID3ReaderException;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentChapterReader;
-import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException;
-
-/**
- * Utility class for getting chapter data from media files.
- */
-public class ChapterUtils {
- private static final String TAG = "ChapterUtils";
-
- private ChapterUtils() {
- }
-
- /**
- * Uses the download URL of a media object of a feeditem to read its ID3
- * chapters.
- */
- public static void readID3ChaptersFromPlayableStreamUrl(Playable p) {
- if (p != null && p.getStreamUrl() != null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
- InputStream in = null;
- try {
- URL url = new URL(p.getStreamUrl());
- ChapterReader reader = new ChapterReader();
-
- in = url.openStream();
- reader.readInputStream(in);
- List<Chapter> chapters = reader.getChapters();
-
- if (chapters != null) {
- Collections
- .sort(chapters, new ChapterStartTimeComparator());
- processChapters(chapters, p);
- if (chaptersValid(chapters)) {
- p.setChapters(chapters);
- Log.i(TAG, "Chapters loaded");
- } else {
- Log.e(TAG, "Chapter data was invalid");
- }
- } else {
- Log.i(TAG, "ChapterReader could not find any ID3 chapters");
- }
- } catch (MalformedURLException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } catch (ID3ReaderException e) {
- e.printStackTrace();
- } finally {
- if (in != null) {
- try {
- in.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- } else {
- Log.e(TAG,
- "Unable to read ID3 chapters: media or download URL was null");
- }
- }
-
- /**
- * Uses the file URL of a media object of a feeditem to read its ID3
- * chapters.
- */
- public static void readID3ChaptersFromPlayableFileUrl(Playable p) {
- if (p != null && p.localFileAvailable() && p.getLocalMediaUrl() != null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
- File source = new File(p.getLocalMediaUrl());
- if (source.exists()) {
- ChapterReader reader = new ChapterReader();
- InputStream in = null;
-
- try {
- in = new BufferedInputStream(new FileInputStream(source));
- reader.readInputStream(in);
- List<Chapter> chapters = reader.getChapters();
-
- if (chapters != null) {
- Collections.sort(chapters,
- new ChapterStartTimeComparator());
- processChapters(chapters, p);
- if (chaptersValid(chapters)) {
- p.setChapters(chapters);
- Log.i(TAG, "Chapters loaded");
- } else {
- Log.e(TAG, "Chapter data was invalid");
- }
- } else {
- Log.i(TAG,
- "ChapterReader could not find any ID3 chapters");
- }
- } catch (IOException e) {
- e.printStackTrace();
- } catch (ID3ReaderException e) {
- e.printStackTrace();
- } finally {
- if (in != null) {
- try {
- in.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- } else {
- Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist");
- }
- }
- }
-
- public static void readOggChaptersFromPlayableStreamUrl(Playable media) {
- if (media != null && media.streamAvailable()) {
- InputStream input = null;
- try {
- URL url = new URL(media.getStreamUrl());
- input = url.openStream();
- if (input != null) {
- readOggChaptersFromInputStream(media, input);
- }
- } catch (MalformedURLException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- IOUtils.closeQuietly(input);
- }
- }
- }
-
- public static void readOggChaptersFromPlayableFileUrl(Playable media) {
- if (media != null && media.getLocalMediaUrl() != null) {
- File source = new File(media.getLocalMediaUrl());
- if (source.exists()) {
- InputStream input = null;
- try {
- input = new BufferedInputStream(new FileInputStream(source));
- readOggChaptersFromInputStream(media, input);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } finally {
- IOUtils.closeQuietly(input);
- }
- }
- }
- }
-
- private static void readOggChaptersFromInputStream(Playable p,
- InputStream input) {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Trying to read chapters from item with title "
- + p.getEpisodeTitle());
- try {
- VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
- reader.readInputStream(input);
- List<Chapter> chapters = reader.getChapters();
- if (chapters != null) {
- Collections.sort(chapters, new ChapterStartTimeComparator());
- processChapters(chapters, p);
- if (chaptersValid(chapters)) {
- p.setChapters(chapters);
- Log.i(TAG, "Chapters loaded");
- } else {
- Log.e(TAG, "Chapter data was invalid");
- }
- } else {
- Log.i(TAG,
- "ChapterReader could not find any Ogg vorbis chapters");
- }
- } catch (VorbisCommentReaderException e) {
- e.printStackTrace();
- }
- }
-
- /**
- * Makes sure that chapter does a title and an item attribute.
- */
- private static void processChapters(List<Chapter> chapters, Playable p) {
- for (int i = 0; i < chapters.size(); i++) {
- Chapter c = chapters.get(i);
- if (c.getTitle() == null) {
- c.setTitle(Integer.toString(i));
- }
- }
- }
-
- private static boolean chaptersValid(List<Chapter> chapters) {
- if (chapters.isEmpty()) {
- return false;
- }
- for (Chapter c : chapters) {
- if (c.getTitle() == null) {
- return false;
- }
- if (c.getStart() < 0) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Calls getCurrentChapter with current position.
- */
- public static Chapter getCurrentChapter(Playable media) {
- if (media.getChapters() != null) {
- List<Chapter> chapters = media.getChapters();
- Chapter current = null;
- if (chapters != null) {
- current = chapters.get(0);
- for (Chapter sc : chapters) {
- if (sc.getStart() > media.getPosition()) {
- break;
- } else {
- current = sc;
- }
- }
- }
- return current;
- } else {
- return null;
- }
- }
-
- public static void loadChaptersFromStreamUrl(Playable media) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Starting chapterLoader thread");
- ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media);
- if (media.getChapters() == null) {
- ChapterUtils.readOggChaptersFromPlayableStreamUrl(media);
- }
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "ChapterLoaderThread has finished");
- }
-
- public static void loadChaptersFromFileUrl(Playable media) {
- if (media.localFileAvailable()) {
- ChapterUtils.readID3ChaptersFromPlayableFileUrl(media);
- if (media.getChapters() == null) {
- ChapterUtils.readOggChaptersFromPlayableFileUrl(media);
- }
- } else {
- Log.e(TAG, "Could not load chapters from file url: local file not available");
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/Converter.java b/src/de/danoeh/antennapod/util/Converter.java
deleted file mode 100644
index f4c2b2f66..000000000
--- a/src/de/danoeh/antennapod/util/Converter.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.util.Log;
-
-/** Provides methods for converting various units. */
-public final class Converter {
- /** Class shall not be instantiated. */
- private Converter() {
- }
-
- /** Logging tag. */
- private static final String TAG = "Converter";
-
-
- /** Indicates that the value is in the Byte range.*/
- private static final int B_RANGE = 0;
- /** Indicates that the value is in the Kilobyte range.*/
- private static final int KB_RANGE = 1;
- /** Indicates that the value is in the Megabyte range.*/
- private static final int MB_RANGE = 2;
- /** Indicates that the value is in the Gigabyte range.*/
- private static final int GB_RANGE = 3;
- /** Determines the length of the number for best readability.*/
- private static final int NUM_LENGTH = 1024;
-
-
- private static final int HOURS_MIL = 3600000;
- private static final int MINUTES_MIL = 60000;
- private static final int SECONDS_MIL = 1000;
-
- /** Takes a byte-value and converts it into a more readable
- * String.
- * @param input The value to convert
- * @return The converted String with a unit
- * */
- public static String byteToString(final long input) {
- int i = 0;
- int result = 0;
-
- for (i = 0; i < GB_RANGE + 1; i++) {
- result = (int) (input / Math.pow(1024, i));
- if (result < NUM_LENGTH) {
- break;
- }
- }
-
- switch (i) {
- case B_RANGE:
- return result + " B";
- case KB_RANGE:
- return result + " KB";
- case MB_RANGE:
- return result + " MB";
- case GB_RANGE:
- return result + " GB";
- default:
- Log.e(TAG, "Error happened in byteToString");
- return "ERROR";
- }
- }
-
- /** Converts milliseconds to a string containing hours, minutes and seconds */
- public static String getDurationStringLong(int duration) {
- int h = duration / HOURS_MIL;
- int rest = duration - h * HOURS_MIL;
- int m = rest / MINUTES_MIL;
- rest -= m * MINUTES_MIL;
- int s = rest / SECONDS_MIL;
-
- return String.format("%02d:%02d:%02d", h, m, s);
- }
-
- /** Converts milliseconds to a string containing hours and minutes */
- public static String getDurationStringShort(int duration) {
- int h = duration / HOURS_MIL;
- int rest = duration - h * HOURS_MIL;
- int m = rest / MINUTES_MIL;
-
- return String.format("%02d:%02d", h, m);
- }
-
- /** Converts long duration string (HH:MM:SS) to milliseconds. */
- public static int durationStringLongToMs(String input) {
- String[] parts = input.split(":");
- if (parts.length != 3) {
- return 0;
- }
- return Integer.valueOf(parts[0]) * 3600 * 1000 +
- Integer.valueOf(parts[1]) * 60 * 1000 +
- Integer.valueOf(parts[2]) * 1000;
- }
-
- /** Converts short duration string (HH:MM) to milliseconds. */
- public static int durationStringShortToMs(String input) {
- String[] parts = input.split(":");
- if (parts.length != 2) {
- return 0;
- }
- return Integer.valueOf(parts[0]) * 3600 * 1000 +
- Integer.valueOf(parts[1]) * 1000 * 60;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/DownloadError.java b/src/de/danoeh/antennapod/util/DownloadError.java
deleted file mode 100644
index 1a64991a6..000000000
--- a/src/de/danoeh/antennapod/util/DownloadError.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.content.Context;
-import de.danoeh.antennapod.R;
-
-/** Utility class for Download Errors. */
-public enum DownloadError {
- SUCCESS(0, R.string.download_successful),
- ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception),
- ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type),
- ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error),
- ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown),
- ERROR_IO_ERROR(5, R.string.download_error_io_error),
- ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown),
- ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown),
- ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found),
- ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error),
- ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space),
- ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host),
- ERROR_REQUEST_ERROR(12, R.string.download_error_request_error),
- ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access),
- ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized);
-
- private final int code;
- private final int resId;
-
- private DownloadError(int code, int resId) {
- this.code = code;
- this.resId = resId;
- }
-
- /** Return DownloadError from its associated code. */
- public static DownloadError fromCode(int code) {
- for (DownloadError reason : values()) {
- if (reason.getCode() == code) {
- return reason;
- }
- }
- throw new IllegalArgumentException("unknown code: " + code);
- }
-
- /** Get machine-readable code. */
- public int getCode() {
- return code;
- }
-
- /** Get a human-readable string. */
- public String getErrorString(Context context) {
- return context.getString(resId);
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/DuckType.java b/src/de/danoeh/antennapod/util/DuckType.java
deleted file mode 100644
index 163110418..000000000
--- a/src/de/danoeh/antennapod/util/DuckType.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/* Adapted from: http://thinking-in-code.blogspot.com/2008/11/duck-typing-in-java-using-dynamic.html */
-
-package de.danoeh.antennapod.util;
-
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-
-import de.danoeh.antennapod.BuildConfig;
-
-/**
- * Allows "duck typing" or dynamic invocation based on method signature rather
- * than type hierarchy. In other words, rather than checking whether something
- * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck.
- *
- * To use first use the coerce static method to indicate the object you want to
- * do Duck Typing for, then specify an interface to the to method which you want
- * to coerce the type to, e.g:
- *
- * public interface Foo { void aMethod(); } class Bar { ... public void
- * aMethod() { ... } ... } Bar bar = ...; Foo foo =
- * DuckType.coerce(bar).to(Foo.class); foo.aMethod();
- *
- *
- */
-public class DuckType {
-
- private final Object objectToCoerce;
-
- private DuckType(Object objectToCoerce) {
- this.objectToCoerce = objectToCoerce;
- }
-
- private class CoercedProxy implements InvocationHandler {
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- Method delegateMethod = findMethodBySignature(method);
- assert delegateMethod != null;
- return delegateMethod.invoke(DuckType.this.objectToCoerce, args);
- }
- }
-
- /**
- * Specify the duck typed object to coerce.
- *
- * @param object
- * the object to coerce
- * @return
- */
- public static DuckType coerce(Object object) {
- return new DuckType(object);
- }
-
- /**
- * Coerce the Duck Typed object to the given interface providing it
- * implements all the necessary methods.
- *
- * @param
- * @param iface
- * @return an instance of the given interface that wraps the duck typed
- * class
- * @throws ClassCastException
- * if the object being coerced does not implement all the
- * methods in the given interface.
- */
- @SuppressWarnings({ "rawtypes", "unchecked" })
- public <T> T to(Class iface) {
- if (BuildConfig.DEBUG && !iface.isInterface()) throw new AssertionError("cannot coerce object to a class, must be an interface");
- if (isA(iface)) {
- return (T) iface.cast(objectToCoerce);
- }
- if (quacksLikeA(iface)) {
- return generateProxy(iface);
- }
- throw new ClassCastException("Could not coerce object of type " + objectToCoerce.getClass() + " to " + iface);
- }
-
- @SuppressWarnings("rawtypes")
- private boolean isA(Class iface) {
- return objectToCoerce.getClass().isInstance(iface);
- }
-
- /**
- * Determine whether the duck typed object can be used with the given
- * interface.
- *
- * @param Type
- * of the interface to check.
- * @param iface
- * Interface class to check
- * @return true if the object will support all the methods in the interface,
- * false otherwise.
- */
- @SuppressWarnings("rawtypes")
- public boolean quacksLikeA(Class iface) {
- for (Method method : iface.getMethods()) {
- if (findMethodBySignature(method) == null) {
- return false;
- }
- }
- return true;
- }
-
- @SuppressWarnings({ "unchecked", "rawtypes" })
- private <T> T generateProxy(Class iface) {
- return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new CoercedProxy());
- }
-
- private Method findMethodBySignature(Method method) {
- try {
- return objectToCoerce.getClass().getMethod(method.getName(), method.getParameterTypes());
- } catch (NoSuchMethodException e) {
- return null;
- }
- }
-
-} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/util/EpisodeFilter.java b/src/de/danoeh/antennapod/util/EpisodeFilter.java
deleted file mode 100644
index 115913bca..000000000
--- a/src/de/danoeh/antennapod/util/EpisodeFilter.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import de.danoeh.antennapod.feed.FeedItem;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class EpisodeFilter {
- private EpisodeFilter() {
-
- }
-
- /** Return a copy of the itemlist without items which have no media. */
- public static ArrayList<FeedItem> getEpisodeList(List<FeedItem> items) {
- ArrayList<FeedItem> episodes = new ArrayList<FeedItem>(items);
- for (FeedItem item : items) {
- if (item.getMedia() == null) {
- episodes.remove(item);
- }
- }
- return episodes;
- }
-
- public static int countItemsWithEpisodes(List<FeedItem> items) {
- int count = 0;
- for (FeedItem item : items) {
- if (item.getMedia() != null) {
- count++;
- }
- }
- return count;
- }
-
- public static FeedItem accessEpisodeByIndex(List<FeedItem> items,
- int position) {
- int count = 0;
- for (FeedItem item : items) {
-
- if (item.getMedia() != null) {
- if (count == position) {
- return item;
- } else {
- count++;
- }
- }
- }
- return null;
- }
-}
diff --git a/src/de/danoeh/antennapod/util/FeedtitleComparator.java b/src/de/danoeh/antennapod/util/FeedtitleComparator.java
deleted file mode 100644
index 112b6678d..000000000
--- a/src/de/danoeh/antennapod/util/FeedtitleComparator.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import de.danoeh.antennapod.feed.Feed;
-
-import java.util.Comparator;
-
-/** Compares the title of two feeds for sorting. */
-public class FeedtitleComparator implements Comparator<Feed> {
-
- @Override
- public int compare(Feed lhs, Feed rhs) {
- return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/FileNameGenerator.java b/src/de/danoeh/antennapod/util/FileNameGenerator.java
deleted file mode 100644
index 702df62b8..000000000
--- a/src/de/danoeh/antennapod/util/FileNameGenerator.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import java.util.Arrays;
-
-/** Generates valid filenames for a given string. */
-public class FileNameGenerator {
-
- private static final char[] ILLEGAL_CHARACTERS = { '/', '\\', '?', '%',
- '*', ':', '|', '"', '<', '>' };
- static {
- Arrays.sort(ILLEGAL_CHARACTERS);
- }
-
- private FileNameGenerator() {
-
- }
-
- /**
- * This method will return a new string that doesn't contain any illegal
- * characters of the given string.
- */
- public static String generateFileName(String string) {
- StringBuilder builder = new StringBuilder();
- for (int i = 0; i < string.length(); i++) {
- char c = string.charAt(i);
- if (Arrays.binarySearch(ILLEGAL_CHARACTERS, c) < 0) {
- builder.append(c);
- }
- }
- return builder.toString().replaceFirst(" *$","");
- }
-
- public static long generateLong(final String str) {
- return str.hashCode();
- }
-}
diff --git a/src/de/danoeh/antennapod/util/InvalidFeedException.java b/src/de/danoeh/antennapod/util/InvalidFeedException.java
deleted file mode 100644
index 50adae216..000000000
--- a/src/de/danoeh/antennapod/util/InvalidFeedException.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.danoeh.antennapod.util;
-
-/** Thrown if a feed has invalid attribute values. */
-public class InvalidFeedException extends Exception {
-
- public InvalidFeedException() {
- }
-
- public InvalidFeedException(String detailMessage) {
- super(detailMessage);
- }
-
- public InvalidFeedException(Throwable throwable) {
- super(throwable);
- }
-
- public InvalidFeedException(String detailMessage, Throwable throwable) {
- super(detailMessage, throwable);
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/LangUtils.java b/src/de/danoeh/antennapod/util/LangUtils.java
deleted file mode 100644
index e6e1d8399..000000000
--- a/src/de/danoeh/antennapod/util/LangUtils.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import java.nio.charset.Charset;
-import java.util.HashMap;
-
-public class LangUtils {
- public static final Charset UTF_8 = Charset.forName("UTF-8");
-
- private static HashMap<String, String> languages;
- static {
- languages = new HashMap<String, String>();
- languages.put("af", "Afrikaans");
- languages.put("sq", "Albanian");
- languages.put("sq", "Albanian");
- languages.put("eu", "Basque");
- languages.put("be", "Belarusian");
- languages.put("bg", "Bulgarian");
- languages.put("ca", "Catalan");
- languages.put("Chinese (Simplified)", "zh-cn");
- languages.put("Chinese (Traditional)", "zh-tw");
- languages.put("hr", "Croatian");
- languages.put("cs", "Czech");
- languages.put("da", "Danish");
- languages.put("nl", "Dutch");
- languages.put("nl-be", "Dutch (Belgium)");
- languages.put("nl-nl", "Dutch (Netherlands)");
- languages.put("en", "English");
- languages.put("en-au", "English (Australia)");
- languages.put("en-bz", "English (Belize)");
- languages.put("en-ca", "English (Canada)");
- languages.put("en-ie", "English (Ireland)");
- languages.put("en-jm", "English (Jamaica)");
- languages.put("en-nz", "English (New Zealand)");
- languages.put("en-ph", "English (Phillipines)");
- languages.put("en-za", "English (South Africa)");
- languages.put("en-tt", "English (Trinidad)");
- languages.put("en-gb", "English (United Kingdom)");
- languages.put("en-us", "English (United States)");
- languages.put("en-zw", "English (Zimbabwe)");
- languages.put("et", "Estonian");
- languages.put("fo", "Faeroese");
- languages.put("fi", "Finnish");
- languages.put("fr", "French");
- languages.put("fr-be", "French (Belgium)");
- languages.put("fr-ca", "French (Canada)");
- languages.put("fr-fr", "French (France)");
- languages.put("fr-lu", "French (Luxembourg)");
- languages.put("fr-mc", "French (Monaco)");
- languages.put("fr-ch", "French (Switzerland)");
- languages.put("gl", "Galician");
- languages.put("gd", "Gaelic");
- languages.put("de", "German");
- languages.put("de-at", "German (Austria)");
- languages.put("de-de", "German (Germany)");
- languages.put("de-li", "German (Liechtenstein)");
- languages.put("de-lu", "German (Luxembourg)");
- languages.put("de-ch", "German (Switzerland)");
- languages.put("el", "Greek");
- languages.put("haw", "Hawaiian");
- languages.put("hu", "Hungarian");
- languages.put("is", "Icelandic");
- languages.put("in", "Indonesian");
- languages.put("ga", "Irish");
- languages.put("it", "Italian");
- languages.put("it-it", "Italian (Italy)");
- languages.put("it-ch", "Italian (Switzerland)");
- languages.put("ja", "Japanese");
- languages.put("ko", "Korean");
- languages.put("mk", "Macedonian");
- languages.put("no", "Norwegian");
- languages.put("pl", "Polish");
- languages.put("pt", "Portugese");
- languages.put("pt-br", "Portugese (Brazil)");
- languages.put("pt-pt", "Portugese (Portugal");
- languages.put("ro", "Romanian");
- languages.put("ro-mo", "Romanian (Moldova)");
- languages.put("ro-ro", "Romanian (Romania");
- languages.put("ru", "Russian");
- languages.put("ru-mo", "Russian (Moldova)");
- languages.put("ru-ru", "Russian (Russia)");
- languages.put("sr", "Serbian");
- languages.put("sk", "Slovak");
- languages.put("sl", "Slovenian");
- languages.put("es", "Spanish");
- languages.put("es-ar", "Spanish (Argentinia)");
- languages.put("es=bo", "Spanish (Bolivia)");
- languages.put("es-cl", "Spanish (Chile)");
- languages.put("es-co", "Spanish (Colombia)");
- languages.put("es-cr", "Spanish (Costa Rica)");
- languages.put("es-do", "Spanish (Dominican Republic)");
- languages.put("es-ec", "Spanish (Ecuador)");
- languages.put("es-sv", "Spanish (El Salvador)");
- languages.put("es-gt", "Spanish (Guatemala)");
- languages.put("es-hn", "Spanish (Honduras)");
- languages.put("es-mx", "Spanish (Mexico)");
- languages.put("es-ni", "Spanish (Nicaragua)");
- languages.put("es-pa", "Spanish (Panama)");
- languages.put("es-py", "Spanish (Paraguay)");
- languages.put("es-pe", "Spanish (Peru)");
- languages.put("es-pr", "Spanish (Puerto Rico)");
- languages.put("es-es", "Spanish (Spain)");
- languages.put("es-uy", "Spanish (Uruguay)");
- languages.put("es-ve", "Spanish (Venezuela)");
- languages.put("sv", "Swedish");
- languages.put("sv-fi", "Swedish (Finland)");
- languages.put("sv-se", "Swedish (Sweden)");
- languages.put("tr", "Turkish");
- languages.put("uk", "Ukranian");
- }
-
- /** Finds language string for key or returns the language key if it can't be found. */
- public static String getLanguageString(String key) {
- String language = languages.get(key);
- if (language != null) {
- return language;
- } else {
- return key;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/NetworkUtils.java b/src/de/danoeh/antennapod/util/NetworkUtils.java
deleted file mode 100644
index 0c8065e94..000000000
--- a/src/de/danoeh/antennapod/util/NetworkUtils.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiManager;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.preferences.UserPreferences;
-
-import java.util.Arrays;
-import java.util.List;
-
-public class NetworkUtils {
- private static final String TAG = "NetworkUtils";
-
- private NetworkUtils() {
-
- }
-
- /**
- * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for
- * automatic downloads is disabled or the device is connected to a Wi-Fi
- * network that is on the 'selected networks' list of the Wi-Fi filter for
- * automatic downloads and false otherwise.
- * */
- public static boolean autodownloadNetworkAvailable(Context context) {
- ConnectivityManager cm = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- if (networkInfo != null) {
- if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Device is connected to Wi-Fi");
- if (networkInfo.isConnected()) {
- if (!UserPreferences.isEnableAutodownloadWifiFilter()) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Auto-dl filter is disabled");
- return true;
- } else {
- WifiManager wm = (WifiManager) context
- .getSystemService(Context.WIFI_SERVICE);
- WifiInfo wifiInfo = wm.getConnectionInfo();
- List<String> selectedNetworks = Arrays
- .asList(UserPreferences
- .getAutodownloadSelectedNetworks());
- if (selectedNetworks.contains(Integer.toString(wifiInfo
- .getNetworkId()))) {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "Current network is on the selected networks list");
- return true;
- }
- }
- }
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Network for auto-dl is not available");
- return false;
- }
-
- public static boolean networkAvailable(Context context) {
- ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo info = cm.getActiveNetworkInfo();
- return info != null && info.isConnected();
- }
-}
diff --git a/src/de/danoeh/antennapod/util/QueueAccess.java b/src/de/danoeh/antennapod/util/QueueAccess.java
deleted file mode 100644
index 7a1c7fef2..000000000
--- a/src/de/danoeh/antennapod/util/QueueAccess.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import de.danoeh.antennapod.feed.FeedItem;
-
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that
- * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List.
- * QueueAccess provides an interface for accessing the queue without having to care about the type of the queue
- * representation.
- */
-public abstract class QueueAccess {
- /**
- * Returns true if the item is in the queue, false otherwise.
- */
- public abstract boolean contains(long id);
-
- /**
- * Removes the item from the queue.
- *
- * @return true if the queue was modified by this operation.
- */
- public abstract boolean remove(long id);
-
- private QueueAccess() {
-
- }
-
- public static QueueAccess IDListAccess(final List<Long> ids) {
- return new QueueAccess() {
- @Override
- public boolean contains(long id) {
- return (ids != null) && ids.contains(id);
- }
-
- @Override
- public boolean remove(long id) {
- return ids.remove(id);
- }
-
-
- };
- }
-
- public static QueueAccess ItemListAccess(final List<FeedItem> items) {
- return new QueueAccess() {
- @Override
- public boolean contains(long id) {
- if (items == null) {
- return false;
- }
- for (FeedItem item : items) {
- if (item.getId() == id) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public boolean remove(long id) {
- Iterator<FeedItem> it = items.iterator();
- FeedItem item;
- while (it.hasNext()) {
- item = it.next();
- if (item.getId() == id) {
- it.remove();
- return true;
- }
- }
- return false;
- }
- };
- }
-
- public static QueueAccess NotInQueueAccess() {
- return new QueueAccess() {
- @Override
- public boolean contains(long id) {
- return false;
- }
-
- @Override
- public boolean remove(long id) {
- return false;
- }
- };
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/ShareUtils.java b/src/de/danoeh/antennapod/util/ShareUtils.java
deleted file mode 100644
index 941fc62ae..000000000
--- a/src/de/danoeh/antennapod/util/ShareUtils.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.content.Context;
-import android.content.Intent;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-
-/** Utility methods for sharing data */
-public class ShareUtils {
- private static final String TAG = "ShareUtils";
-
- private ShareUtils() {}
-
- public static void shareLink(Context context, String link) {
- Intent i = new Intent(Intent.ACTION_SEND);
- i.setType("text/plain");
- i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL");
- i.putExtra(Intent.EXTRA_TEXT, link);
- context.startActivity(Intent.createChooser(i, "Share URL"));
- }
-
- public static void shareFeedItemLink(Context context, FeedItem item) {
- shareLink(context, item.getLink());
- }
-
- public static void shareFeedDownloadLink(Context context, Feed feed) {
- shareLink(context, feed.getDownload_url());
- }
-
- public static void shareFeedlink(Context context, Feed feed) {
- shareLink(context, feed.getLink());
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/ShownotesProvider.java b/src/de/danoeh/antennapod/util/ShownotesProvider.java
deleted file mode 100644
index 8345ca34d..000000000
--- a/src/de/danoeh/antennapod/util/ShownotesProvider.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import java.util.concurrent.Callable;
-
-/**
- * Created by daniel on 04.08.13.
- */
-public interface ShownotesProvider {
- /**
- * Loads shownotes. If the shownotes have to be loaded from a file or from a
- * database, it should be done in a separate thread. After the shownotes
- * have been loaded, callback.onShownotesLoaded should be called.
- */
- public Callable<String> loadShownotes();
-
-}
diff --git a/src/de/danoeh/antennapod/util/StorageUtils.java b/src/de/danoeh/antennapod/util/StorageUtils.java
deleted file mode 100644
index ff0bde280..000000000
--- a/src/de/danoeh/antennapod/util/StorageUtils.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.StatFs;
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.activity.StorageErrorActivity;
-import de.danoeh.antennapod.preferences.UserPreferences;
-
-import java.io.File;
-
-/** Utility functions for handling storage errors */
-public class StorageUtils {
- private static final String TAG = "StorageUtils";
-
- public static boolean storageAvailable(Context context) {
- File dir = UserPreferences.getDataFolder(context, null);
- if (dir != null) {
- return dir.exists() && dir.canRead() && dir.canWrite();
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Storage not available: data folder is null");
- return false;
- }
- }
-
- /**
- * Checks if external storage is available. If external storage isn't
- * available, the current activity is finsished an an error activity is
- * launched.
- *
- * @param activity
- * the activity which would be finished if no storage is
- * available
- * @return true if external storage is available
- */
- public static boolean checkStorageAvailability(Activity activity) {
- boolean storageAvailable = storageAvailable(activity);
- if (!storageAvailable) {
- activity.finish();
- activity.startActivity(new Intent(activity,
- StorageErrorActivity.class));
- }
- return storageAvailable;
- }
-
- /** Get the number of free bytes that are available on the external storage. */
- public static long getFreeSpaceAvailable() {
- StatFs stat = new StatFs(UserPreferences.getDataFolder(
- PodcastApp.getInstance(), null).getAbsolutePath());
- long availableBlocks;
- long blockSize;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- availableBlocks = stat.getAvailableBlocksLong();
- blockSize = stat.getBlockSizeLong();
- } else {
- availableBlocks = stat.getAvailableBlocks();
- blockSize = stat.getBlockSize();
- }
- return availableBlocks * blockSize;
- }
-}
diff --git a/src/de/danoeh/antennapod/util/ThemeUtils.java b/src/de/danoeh/antennapod/util/ThemeUtils.java
deleted file mode 100644
index 8e593f3fb..000000000
--- a/src/de/danoeh/antennapod/util/ThemeUtils.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.util.Log;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.preferences.UserPreferences;
-
-public class ThemeUtils {
- private static final String TAG = "ThemeUtils";
-
- public static int getSelectionBackgroundColor() {
- switch (UserPreferences.getTheme()) {
- case R.style.Theme_AntennaPod_Dark:
- return R.color.selection_background_color_dark;
- case R.style.Theme_AntennaPod_Light:
- return R.color.selection_background_color_light;
- default:
- Log.e(TAG,
- "getSelectionBackgroundColor could not match the current theme to any color!");
- return R.color.selection_background_color_light;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/URIUtil.java b/src/de/danoeh/antennapod/util/URIUtil.java
deleted file mode 100644
index 5af40d591..000000000
--- a/src/de/danoeh/antennapod/util/URIUtil.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-
-/**
- * Utility methods for dealing with URL encoding.
- */
-public class URIUtil {
- private static final String TAG = "URIUtil";
-
- private URIUtil() {}
-
- public static URI getURIFromRequestUrl(String source) {
- // try without encoding the URI
- try {
- return new URI(source);
- } catch (URISyntaxException e) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Source is not encoded, encoding now");
- }
- try {
- URL url = new URL(source);
- return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
- } catch (MalformedURLException e) {
- throw new IllegalArgumentException(e);
- } catch (URISyntaxException e) {
- throw new IllegalArgumentException(e);
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/URLChecker.java b/src/de/danoeh/antennapod/util/URLChecker.java
deleted file mode 100644
index 2352adddf..000000000
--- a/src/de/danoeh/antennapod/util/URLChecker.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.danoeh.antennapod.util;
-
-import android.util.Log;
-
-import org.apache.commons.lang3.StringUtils;
-
-import de.danoeh.antennapod.BuildConfig;
-
-/**
- * Provides methods for checking and editing a URL.
- */
-public final class URLChecker {
-
- /**
- * Class shall not be instantiated.
- */
- private URLChecker() {
- }
-
- /**
- * Logging tag.
- */
- private static final String TAG = "URLChecker";
-
- private static final String AP_SUBSCRIBE = "antennapod-subscribe://";
-
- /**
- * Checks if URL is valid and modifies it if necessary.
- *
- * @param url The url which is going to be prepared
- * @return The prepared url
- */
- public static String prepareURL(String url) {
- url = StringUtils.trim(url);
- if (url.startsWith("feed://")) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://");
- return url.replaceFirst("feed://", "http://");
- } else if (url.startsWith("pcast://")) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Removing pcast://");
- return prepareURL(StringUtils.removeStart(url, "pcast://"));
- } else if (url.startsWith("itpc")) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://");
- return url.replaceFirst("itpc://", "http://");
- } else if (url.startsWith(AP_SUBSCRIBE)) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Removing antennapod-subscribe://");
- return prepareURL(StringUtils.removeStart(url, AP_SUBSCRIBE));
- } else if (!(url.startsWith("http://") || url.startsWith("https://"))) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL");
- return "http://" + url;
- } else {
- return url;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/UndoBarController.java b/src/de/danoeh/antennapod/util/UndoBarController.java
deleted file mode 100644
index 332cc22e0..000000000
--- a/src/de/danoeh/antennapod/util/UndoBarController.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright 2012 Roman Nurik
- *
- * 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.
- */
-
-package de.danoeh.antennapod.util;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.view.View;
-import android.widget.TextView;
-import com.nineoldandroids.animation.Animator;
-import com.nineoldandroids.animation.AnimatorListenerAdapter;
-import com.nineoldandroids.view.ViewHelper;
-import com.nineoldandroids.view.ViewPropertyAnimator;
-import de.danoeh.antennapod.R;
-
-import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
-
-public class UndoBarController {
- private View mBarView;
- private TextView mMessageView;
- private ViewPropertyAnimator mBarAnimator;
- private Handler mHideHandler = new Handler();
-
- private UndoListener mUndoListener;
-
- // State objects
- private Parcelable mUndoToken;
- private CharSequence mUndoMessage;
-
- public interface UndoListener {
- void onUndo(Parcelable token);
- }
-
- public UndoBarController(View undoBarView, UndoListener undoListener) {
- mBarView = undoBarView;
- mBarAnimator = animate(mBarView);
- mUndoListener = undoListener;
-
- mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message);
- mBarView.findViewById(R.id.undobar_button)
- .setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- hideUndoBar(false);
- mUndoListener.onUndo(mUndoToken);
- }
- });
-
- hideUndoBar(true);
- }
-
- public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) {
- mUndoToken = undoToken;
- mUndoMessage = message;
- mMessageView.setText(mUndoMessage);
-
- mHideHandler.removeCallbacks(mHideRunnable);
- mHideHandler.postDelayed(mHideRunnable,
- mBarView.getResources().getInteger(R.integer.undobar_hide_delay));
-
- mBarView.setVisibility(View.VISIBLE);
- if (immediate) {
- ViewHelper.setAlpha(mBarView, 1);
- } else {
- mBarAnimator.cancel();
- mBarAnimator
- .alpha(1)
- .setDuration(
- mBarView.getResources()
- .getInteger(android.R.integer.config_shortAnimTime))
- .setListener(null);
- }
- }
-
- public void hideUndoBar(boolean immediate) {
- mHideHandler.removeCallbacks(mHideRunnable);
- if (immediate) {
- mBarView.setVisibility(View.GONE);
- ViewHelper.setAlpha(mBarView, 0);
- mUndoMessage = null;
- mUndoToken = null;
-
- } else {
- mBarAnimator.cancel();
- mBarAnimator
- .alpha(0)
- .setDuration(mBarView.getResources()
- .getInteger(android.R.integer.config_shortAnimTime))
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mBarView.setVisibility(View.GONE);
- mUndoMessage = null;
- mUndoToken = null;
- }
- });
- }
- }
-
- public void onSaveInstanceState(Bundle outState) {
- outState.putCharSequence("undo_message", mUndoMessage);
- outState.putParcelable("undo_token", mUndoToken);
- }
-
- public void onRestoreInstanceState(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mUndoMessage = savedInstanceState.getCharSequence("undo_message");
- mUndoToken = savedInstanceState.getParcelable("undo_token");
-
- if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) {
- showUndoBar(true, mUndoMessage, mUndoToken);
- }
- }
- }
-
- private Runnable mHideRunnable = new Runnable() {
- @Override
- public void run() {
- hideUndoBar(false);
- }
- };
-}
diff --git a/src/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java b/src/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java
deleted file mode 100644
index bfc2fd057..000000000
--- a/src/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package de.danoeh.antennapod.util.comparator;
-
-import de.danoeh.antennapod.feed.Chapter;
-
-import java.util.Comparator;
-
-public class ChapterStartTimeComparator implements Comparator<Chapter> {
-
- @Override
- public int compare(Chapter lhs, Chapter rhs) {
- if (lhs.getStart() == rhs.getStart()) {
- return 0;
- } else if (lhs.getStart() < rhs.getStart()) {
- return -1;
- } else {
- return 1;
- }
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java b/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java
deleted file mode 100644
index 14b8f1194..000000000
--- a/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.danoeh.antennapod.util.comparator;
-
-import de.danoeh.antennapod.service.download.DownloadStatus;
-
-import java.util.Comparator;
-
-/** Compares the completion date of two Downloadstatus objects. */
-public class DownloadStatusComparator implements Comparator<DownloadStatus> {
-
- @Override
- public int compare(DownloadStatus lhs, DownloadStatus rhs) {
- return rhs.getCompletionDate().compareTo(lhs.getCompletionDate());
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java b/src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java
deleted file mode 100644
index f92c23d05..000000000
--- a/src/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.danoeh.antennapod.util.comparator;
-
-import de.danoeh.antennapod.feed.FeedItem;
-
-import java.util.Comparator;
-
-/** Compares the pubDate of two FeedItems for sorting*/
-public class FeedItemPubdateComparator implements Comparator<FeedItem> {
-
- /** Returns a new instance of this comparator in reverse order.
- public static FeedItemPubdateComparator newInstance() {
- FeedItemPubdateComparator
- }*/
- @Override
- public int compare(FeedItem lhs, FeedItem rhs) {
- return rhs.getPubDate().compareTo(lhs.getPubDate());
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java b/src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java
deleted file mode 100644
index 0147e0cdc..000000000
--- a/src/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.danoeh.antennapod.util.comparator;
-
-import de.danoeh.antennapod.feed.FeedItem;
-
-import java.util.Comparator;
-
-public class PlaybackCompletionDateComparator implements Comparator<FeedItem> {
-
- public int compare(FeedItem lhs, FeedItem rhs) {
- if (lhs.getMedia() != null
- && lhs.getMedia().getPlaybackCompletionDate() != null
- && rhs.getMedia() != null
- && rhs.getMedia().getPlaybackCompletionDate() != null) {
- return rhs.getMedia().getPlaybackCompletionDate()
- .compareTo(lhs.getMedia().getPlaybackCompletionDate());
- }
- return 0;
- }
-}
diff --git a/src/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java b/src/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java
deleted file mode 100644
index 02b084a01..000000000
--- a/src/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.danoeh.antennapod.util.comparator;
-
-import de.danoeh.antennapod.feed.SearchResult;
-
-import java.util.Comparator;
-
-public class SearchResultValueComparator implements Comparator<SearchResult> {
-
- @Override
- public int compare(SearchResult lhs, SearchResult rhs) {
- return rhs.getValue() - lhs.getValue();
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java b/src/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java
deleted file mode 100644
index 4586cea87..000000000
--- a/src/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package de.danoeh.antennapod.util.exception;
-
-import de.danoeh.antennapod.feed.FeedMedia;
-
-public class MediaFileNotFoundException extends Exception {
- private static final long serialVersionUID = 1L;
-
- private FeedMedia media;
-
- public MediaFileNotFoundException(String msg, FeedMedia media) {
- super(msg);
- this.media = media;
- }
-
- public MediaFileNotFoundException(FeedMedia media) {
- super();
- this.media = media;
- }
-
- public FeedMedia getMedia() {
- return media;
- }
-}
diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java b/src/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java
deleted file mode 100644
index eda83b7aa..000000000
--- a/src/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package de.danoeh.antennapod.util.flattr;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import org.shredzone.flattr4j.FlattrFactory;
-import org.shredzone.flattr4j.FlattrService;
-import org.shredzone.flattr4j.oauth.AccessToken;
-
-/** Ensures that only one instance of the FlattrService class exists at a time */
-
-public class FlattrServiceCreator {
- public static final String TAG = "FlattrServiceCreator";
-
- private static volatile FlattrService flattrService;
-
- public static FlattrService getService(AccessToken token) {
- return FlattrFactory.getInstance().createFlattrService(token);
- }
-
- public static void deleteFlattrService() {
- if (BuildConfig.DEBUG) Log.d(TAG, "Deleting service instance");
- flattrService = null;
- }
-}
-
diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java b/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java
deleted file mode 100644
index a1d6d3bc4..000000000
--- a/src/de/danoeh/antennapod/util/flattr/FlattrStatus.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package de.danoeh.antennapod.util.flattr;
-
-import java.util.Calendar;
-
-public class FlattrStatus {
- public static final int STATUS_UNFLATTERED = 0;
- public static final int STATUS_QUEUE = 1;
- public static final int STATUS_FLATTRED = 2;
-
- private int status = STATUS_UNFLATTERED;
- private Calendar lastFlattred;
-
- public FlattrStatus() {
- status = STATUS_UNFLATTERED;
- lastFlattred = Calendar.getInstance();
- }
-
- public FlattrStatus(long status) {
- lastFlattred = Calendar.getInstance();
- fromLong(status);
- }
-
- public void setFlattred() {
- status = STATUS_FLATTRED;
- lastFlattred = Calendar.getInstance();
- }
-
- public void setUnflattred() {
- status = STATUS_UNFLATTERED;
- }
-
- public boolean getUnflattred() {
- return status == STATUS_UNFLATTERED;
- }
-
- public void setFlattrQueue() {
- if (flattrable())
- status = STATUS_QUEUE;
- }
-
- public void fromLong(long status) {
- if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE)
- this.status = (int) status;
- else {
- this.status = STATUS_FLATTRED;
- lastFlattred.setTimeInMillis(status);
- }
- }
-
- public long toLong() {
- if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE)
- return status;
- else {
- return lastFlattred.getTimeInMillis();
- }
- }
-
- public boolean flattrable() {
- Calendar firstOfMonth = Calendar.getInstance();
- firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH));
-
- return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) );
- }
-
- public boolean getFlattrQueue() {
- return status == STATUS_QUEUE;
- }
-}
diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrThing.java b/src/de/danoeh/antennapod/util/flattr/FlattrThing.java
deleted file mode 100644
index f17ef1d83..000000000
--- a/src/de/danoeh/antennapod/util/flattr/FlattrThing.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.danoeh.antennapod.util.flattr;
-
-public interface FlattrThing {
- public String getTitle();
- public String getPaymentLink();
- public FlattrStatus getFlattrStatus();
-}
diff --git a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java b/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java
deleted file mode 100644
index 96d3bbedd..000000000
--- a/src/de/danoeh/antennapod/util/flattr/FlattrUtils.java
+++ /dev/null
@@ -1,305 +0,0 @@
-package de.danoeh.antennapod.util.flattr;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.preference.PreferenceManager;
-import android.util.Log;
-
-import org.apache.commons.lang3.StringUtils;
-import org.shredzone.flattr4j.FlattrService;
-import org.shredzone.flattr4j.exception.FlattrException;
-import org.shredzone.flattr4j.model.Flattr;
-import org.shredzone.flattr4j.model.Thing;
-import org.shredzone.flattr4j.oauth.AccessToken;
-import org.shredzone.flattr4j.oauth.AndroidAuthenticator;
-import org.shredzone.flattr4j.oauth.Scope;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.TimeZone;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.FlattrAuthActivity;
-import de.danoeh.antennapod.asynctask.FlattrTokenFetcher;
-import de.danoeh.antennapod.storage.DBWriter;
-
-/**
- * Utility methods for doing something with flattr.
- */
-
-public class FlattrUtils {
- private static final String TAG = "FlattrUtils";
-
- private static final String HOST_NAME = "de.danoeh.antennapod";
-
- private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken";
-
- // Flattr URL for this app.
- public static final String APP_URL = "http://antennapod.com";
- // Human-readable flattr-page.
- public static final String APP_LINK = "https://flattr.com/thing/745609/";
- public static final String APP_THING_ID = "745609";
-
- private static volatile AccessToken cachedToken;
-
- private static AndroidAuthenticator createAuthenticator() {
- return new AndroidAuthenticator(HOST_NAME, BuildConfig.FLATTR_APP_KEY,
- BuildConfig.FLATTR_APP_SECRET);
- }
-
- public static void startAuthProcess(Context context) throws FlattrException {
- AndroidAuthenticator auth = createAuthenticator();
- auth.setScope(EnumSet.of(Scope.FLATTR));
- Intent intent = auth.createAuthenticateIntent();
- context.startActivity(intent);
- }
-
- private static AccessToken retrieveToken() {
- if (cachedToken == null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Retrieving access token");
- String token = PreferenceManager.getDefaultSharedPreferences(
- PodcastApp.getInstance())
- .getString(PREF_ACCESS_TOKEN, null);
- if (token != null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Found access token. Caching.");
- cachedToken = new AccessToken(token);
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "No access token found");
- return null;
- }
- }
- return cachedToken;
-
- }
-
- /**
- * Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty
- */
- public static boolean hasAPICredentials() {
- return StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_KEY)
- && StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_SECRET);
- }
-
- public static boolean hasToken() {
- return retrieveToken() != null;
- }
-
- public static void storeToken(AccessToken token) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Storing token");
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(PodcastApp.getInstance()).edit();
- if (token != null) {
- editor.putString(PREF_ACCESS_TOKEN, token.getToken());
- } else {
- editor.putString(PREF_ACCESS_TOKEN, null);
- }
- editor.commit();
- cachedToken = token;
- }
-
- public static void deleteToken() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Deleting flattr token");
- storeToken(null);
- }
-
- public static Thing getAppThing(Context context) {
- FlattrService fs = FlattrServiceCreator.getService(retrieveToken());
- try {
- Thing thing = fs.getThing(Thing.withId(APP_THING_ID));
- return thing;
- } catch (FlattrException e) {
- e.printStackTrace();
- showErrorDialog(context, e.getMessage());
- return null;
- }
- }
-
- public static void clickUrl(Context context, String url)
- throws FlattrException {
- if (hasToken()) {
- FlattrService fs = FlattrServiceCreator.getService(retrieveToken());
- fs.flattr(url);
- } else {
- Log.e(TAG, "clickUrl was called with null access token");
- }
- }
-
- public static List<Flattr> retrieveFlattredThings()
- throws FlattrException {
- ArrayList<Flattr> myFlattrs = new ArrayList<Flattr>();
-
- if (hasToken()) {
- FlattrService fs = FlattrServiceCreator.getService(retrieveToken());
-
- Calendar firstOfMonth = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
- firstOfMonth.set(Calendar.MILLISECOND, 0);
- firstOfMonth.set(Calendar.SECOND, 0);
- firstOfMonth.set(Calendar.MINUTE, 0);
- firstOfMonth.set(Calendar.HOUR_OF_DAY, 0);
- firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH));
-
- Date firstOfMonthDate = firstOfMonth.getTime();
-
- // subscriptions some times get flattrd slightly before midnight - give it an hour leeway
- firstOfMonthDate = new Date(firstOfMonthDate.getTime() - 60 * 60 * 1000);
-
- final int FLATTR_COUNT = 30;
- final int FLATTR_MAXPAGE = 5;
-
- for (int page = 0; page < FLATTR_MAXPAGE; page++) {
- for (Flattr fl : fs.getMyFlattrs(FLATTR_COUNT, page)) {
- if (fl.getCreated().after(firstOfMonthDate))
- myFlattrs.add(fl);
- else
- break;
- }
- }
-
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate);
-
- for (Flattr fl : myFlattrs) {
- Thing thing = fl.getThing();
- Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated());
- }
- }
-
- } else {
- Log.e(TAG, "retrieveFlattrdThings was called with null access token");
- }
-
- return myFlattrs;
- }
-
- public static void handleCallback(Context context, Uri uri) {
- AndroidAuthenticator auth = createAuthenticator();
- new FlattrTokenFetcher(context, auth, uri).executeAsync();
- }
-
- public static void revokeAccessToken(Context context) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Revoking access token");
- deleteToken();
- FlattrServiceCreator.deleteFlattrService();
- showRevokeDialog(context);
- DBWriter.clearAllFlattrStatus(context);
- }
-
- // ------------------------------------------------ DIALOGS
-
- public static void showRevokeDialog(final Context context) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.access_revoked_title);
- builder.setMessage(R.string.access_revoked_info);
- builder.setNeutralButton(android.R.string.ok, new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.cancel();
- }
- });
- builder.create().show();
- }
-
- /**
- * Opens a dialog that ask the user to either connect the app with flattr or to be redirected to
- * the thing's website.
- * If no API credentials are available, the user will immediately be redirected to the thing's website.
- * */
- public static void showNoTokenDialogOrRedirect(final Context context, final String url) {
- if (hasAPICredentials()) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.no_flattr_token_title);
- builder.setMessage(R.string.no_flattr_token_msg);
- builder.setPositiveButton(R.string.authenticate_now_label,
- new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- context.startActivity(new Intent(context,
- FlattrAuthActivity.class));
- }
-
- }
- );
-
- builder.setNegativeButton(R.string.visit_website_label,
- new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Uri uri = Uri.parse(url);
- context.startActivity(new Intent(Intent.ACTION_VIEW,
- uri));
- }
-
- }
- );
- builder.create().show();
- } else {
- Uri uri = Uri.parse(url);
- context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
- }
- }
-
- public static void showForbiddenDialog(final Context context,
- final String url) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.action_forbidden_title);
- builder.setMessage(R.string.action_forbidden_msg);
- builder.setPositiveButton(R.string.authenticate_now_label,
- new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- context.startActivity(new Intent(context,
- FlattrAuthActivity.class));
- }
-
- }
- );
- builder.setNegativeButton(R.string.visit_website_label,
- new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Uri uri = Uri.parse(url);
- context.startActivity(new Intent(Intent.ACTION_VIEW,
- uri));
- }
-
- }
- );
- builder.create().show();
- }
-
- public static void showErrorDialog(final Context context, final String msg) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.error_label);
- builder.setMessage(msg);
- builder.setNeutralButton(android.R.string.ok, new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.cancel();
- }
- });
- builder.create().show();
- }
-
-} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java b/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java
deleted file mode 100644
index 296610871..000000000
--- a/src/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package de.danoeh.antennapod.util.flattr;
-
-/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */
-public class SimpleFlattrThing implements FlattrThing {
- public SimpleFlattrThing(String title, String url, FlattrStatus status)
- {
- this.title = title;
- this.url = url;
- this.status = status;
- }
-
- public String getTitle()
- {
- return this.title;
- }
-
- public String getPaymentLink()
- {
- return this.url;
- }
-
- public FlattrStatus getFlattrStatus()
- {
- return this.status;
- }
-
- private String title;
- private String url;
- private FlattrStatus status;
-}
diff --git a/src/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java b/src/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java
deleted file mode 100644
index b920559db..000000000
--- a/src/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package de.danoeh.antennapod.util.gui;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import de.danoeh.antennapod.feed.FeedItem;
-
-/**
- * Used by an UndoBarController for saving a removed FeedItem
- */
-public class FeedItemUndoToken implements Parcelable {
- private long itemId;
- private long feedId;
- private int position;
-
- public FeedItemUndoToken(FeedItem item, int position) {
- this.itemId = item.getId();
- this.feedId = item.getFeed().getId();
- this.position = position;
- }
-
- private FeedItemUndoToken(Parcel in) {
- itemId = in.readLong();
- feedId = in.readLong();
- position = in.readInt();
- }
-
- public static final Parcelable.Creator<FeedItemUndoToken> CREATOR = new Parcelable.Creator<FeedItemUndoToken>() {
- public FeedItemUndoToken createFromParcel(Parcel in) {
- return new FeedItemUndoToken(in);
- }
-
- public FeedItemUndoToken[] newArray(int size) {
- return new FeedItemUndoToken[size];
- }
- };
-
- public int describeContents() {
- return 0;
- }
-
- public void writeToParcel(Parcel out, int flags) {
- out.writeLong(itemId);
- out.writeLong(feedId);
- out.writeInt(position);
- }
-
- public long getFeedItemId() {
- return itemId;
- }
-
- public int getPosition() {
- return position;
- }
-}
-
diff --git a/src/de/danoeh/antennapod/util/id3reader/ChapterReader.java b/src/de/danoeh/antennapod/util/id3reader/ChapterReader.java
deleted file mode 100644
index 257635129..000000000
--- a/src/de/danoeh/antennapod/util/id3reader/ChapterReader.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package de.danoeh.antennapod.util.id3reader;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.ID3Chapter;
-import de.danoeh.antennapod.util.id3reader.model.FrameHeader;
-import de.danoeh.antennapod.util.id3reader.model.TagHeader;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URLDecoder;
-import java.util.ArrayList;
-import java.util.List;
-
-public class ChapterReader extends ID3Reader {
- private static final String TAG = "ID3ChapterReader";
-
- private static final String FRAME_ID_CHAPTER = "CHAP";
- private static final String FRAME_ID_TITLE = "TIT2";
- private static final String FRAME_ID_LINK = "WXXX";
-
- private List<Chapter> chapters;
- private ID3Chapter currentChapter;
-
- @Override
- public int onStartTagHeader(TagHeader header) {
- chapters = new ArrayList<Chapter>();
- System.out.println(header.toString());
- return ID3Reader.ACTION_DONT_SKIP;
- }
-
- @Override
- public int onStartFrameHeader(FrameHeader header, InputStream input)
- throws IOException, ID3ReaderException {
- System.out.println(header.toString());
- if (header.getId().equals(FRAME_ID_CHAPTER)) {
- if (currentChapter != null) {
- if (!hasId3Chapter(currentChapter)) {
- chapters.add(currentChapter);
- if (BuildConfig.DEBUG) Log.d(TAG, "Found chapter: " + currentChapter);
- currentChapter = null;
- }
- }
- StringBuffer elementId = new StringBuffer();
- readISOString(elementId, input, Integer.MAX_VALUE);
- char[] startTimeSource = readBytes(input, 4);
- long startTime = ((int) startTimeSource[0] << 24)
- | ((int) startTimeSource[1] << 16)
- | ((int) startTimeSource[2] << 8) | startTimeSource[3];
- currentChapter = new ID3Chapter(elementId.toString(), startTime);
- skipBytes(input, 12);
- return ID3Reader.ACTION_DONT_SKIP;
- } else if (header.getId().equals(FRAME_ID_TITLE)) {
- if (currentChapter != null && currentChapter.getTitle() == null) {
- StringBuffer title = new StringBuffer();
- readString(title, input, header.getSize());
- currentChapter
- .setTitle(title.toString());
- if (BuildConfig.DEBUG) Log.d(TAG, "Found title: " + currentChapter.getTitle());
-
- return ID3Reader.ACTION_DONT_SKIP;
- }
- } else if (header.getId().equals(FRAME_ID_LINK)) {
- if (currentChapter != null) {
- // skip description
- int descriptionLength = readString(null, input, header.getSize());
- StringBuffer link = new StringBuffer();
- readISOString(link, input, header.getSize() - descriptionLength);
- String decodedLink = URLDecoder.decode(link.toString(), "UTF-8");
-
- currentChapter.setLink(decodedLink);
-
- if (BuildConfig.DEBUG) Log.d(TAG, "Found link: " + currentChapter.getLink());
- return ID3Reader.ACTION_DONT_SKIP;
- }
- } else if (header.getId().equals("APIC")) {
- Log.d(TAG, header.toString());
- }
-
- return super.onStartFrameHeader(header, input);
- }
-
- private boolean hasId3Chapter(ID3Chapter chapter) {
- for (Chapter c : chapters) {
- if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public void onEndTag() {
- if (currentChapter != null) {
- if (!hasId3Chapter(currentChapter)) {
- chapters.add(currentChapter);
- }
- }
- System.out.println("Reached end of tag");
- if (chapters != null) {
- for (Chapter c : chapters) {
- System.out.println(c.toString());
- }
- }
- }
-
- @Override
- public void onNoTagHeaderFound() {
- System.out.println("No tag header found");
- super.onNoTagHeaderFound();
- }
-
- public List<Chapter> getChapters() {
- return chapters;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/id3reader/ID3Reader.java b/src/de/danoeh/antennapod/util/id3reader/ID3Reader.java
deleted file mode 100644
index 252d64107..000000000
--- a/src/de/danoeh/antennapod/util/id3reader/ID3Reader.java
+++ /dev/null
@@ -1,250 +0,0 @@
-package de.danoeh.antennapod.util.id3reader;
-
-import de.danoeh.antennapod.util.id3reader.model.FrameHeader;
-import de.danoeh.antennapod.util.id3reader.model.TagHeader;
-import org.apache.commons.io.IOUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-
-/**
- * Reads the ID3 Tag of a given file. In order to use this class, you should
- * create a subclass of it and overwrite the onStart* - or onEnd* - methods.
- */
-public class ID3Reader {
- private static final int HEADER_LENGTH = 10;
- private static final int ID3_LENGTH = 3;
- private static final int FRAME_ID_LENGTH = 4;
-
- protected static final int ACTION_SKIP = 1;
- protected static final int ACTION_DONT_SKIP = 2;
-
- protected int readerPosition;
-
- private static final byte ENCODING_UTF16_WITH_BOM = 1;
- private static final byte ENCODING_UTF16_WITHOUT_BOM = 2;
- private static final byte ENCODING_UTF8 = 3;
-
- private TagHeader tagHeader;
-
- public ID3Reader() {
- }
-
- public final void readInputStream(InputStream input) throws IOException,
- ID3ReaderException {
- int rc;
- readerPosition = 0;
- char[] tagHeaderSource = readBytes(input, HEADER_LENGTH);
- tagHeader = createTagHeader(tagHeaderSource);
- if (tagHeader == null) {
- onNoTagHeaderFound();
- } else {
- rc = onStartTagHeader(tagHeader);
- if (rc == ACTION_SKIP) {
- onEndTag();
- } else {
- while (readerPosition < tagHeader.getSize()) {
- FrameHeader frameHeader = createFrameHeader(readBytes(
- input, HEADER_LENGTH));
- if (checkForNullString(frameHeader.getId())) {
- break;
- } else {
- rc = onStartFrameHeader(frameHeader, input);
- if (rc == ACTION_SKIP) {
-
- if (frameHeader.getSize() + readerPosition > tagHeader
- .getSize()) {
- break;
- } else {
- skipBytes(input, frameHeader.getSize());
- }
- }
- }
- }
- onEndTag();
- }
- }
- }
-
- /** Returns true if string only contains null-bytes. */
- private boolean checkForNullString(String s) {
- if (!s.isEmpty()) {
- int i = 0;
- if (s.charAt(i) == 0) {
- for (i = 1; i < s.length(); i++) {
- if (s.charAt(i) != 0) {
- return false;
- }
- }
- return true;
- }
- return false;
- } else {
- return true;
- }
-
- }
-
- /**
- * Read a certain number of bytes from the given input stream. This method
- * changes the readerPosition-attribute.
- */
- protected char[] readBytes(InputStream input, int number)
- throws IOException, ID3ReaderException {
- char[] header = new char[number];
- for (int i = 0; i < number; i++) {
- int b = input.read();
- readerPosition++;
- if (b != -1) {
- header[i] = (char) b;
- } else {
- throw new ID3ReaderException("Unexpected end of stream");
- }
- }
- return header;
- }
-
- /**
- * Skip a certain number of bytes on the given input stream. This method
- * changes the readerPosition-attribute.
- */
- protected void skipBytes(InputStream input, int number) throws IOException {
- if (number <= 0) {
- number = 1;
- }
- IOUtils.skipFully(input, number);
-
- readerPosition += number;
- }
-
- private TagHeader createTagHeader(char[] source) throws ID3ReaderException {
- boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44)
- && (source[2] == 0x33);
- if (source.length != HEADER_LENGTH) {
- throw new ID3ReaderException("Length of header must be "
- + HEADER_LENGTH);
- }
- if (hasTag) {
- String id = new String(source, 0, ID3_LENGTH);
- char version = (char) ((source[3] << 8) | source[4]);
- byte flags = (byte) source[5];
- int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8)
- | source[9];
- size = unsynchsafe(size);
- return new TagHeader(id, size, version, flags);
- } else {
- return null;
- }
- }
-
- private FrameHeader createFrameHeader(char[] source)
- throws ID3ReaderException {
- if (source.length != HEADER_LENGTH) {
- throw new ID3ReaderException("Length of header must be "
- + HEADER_LENGTH);
- }
- String id = new String(source, 0, FRAME_ID_LENGTH);
-
- int size = (((int) source[4]) << 24) | (((int) source[5]) << 16)
- | (((int) source[6]) << 8) | source[7];
- if (tagHeader != null && tagHeader.getVersion() >= 0x0400) {
- size = unsynchsafe(size);
- }
- char flags = (char) ((source[8] << 8) | source[9]);
- return new FrameHeader(id, size, flags);
- }
-
- private int unsynchsafe(int in) {
- int out = 0;
- int mask = 0x7F000000;
-
- while (mask != 0) {
- out >>= 1;
- out |= in & mask;
- mask >>= 8;
- }
-
- return out;
- }
-
- protected int readString(StringBuffer buffer, InputStream input, int max) throws IOException,
- ID3ReaderException {
- if (max > 0) {
- char[] encoding = readBytes(input, 1);
- max--;
-
- if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) {
- return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account
- } else if (encoding[0] == ENCODING_UTF8) {
- return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account
- } else {
- return readISOString(buffer, input, max) + 1; // take encoding byte into account
- }
- } else {
- if (buffer != null) {
- buffer.append("");
- }
- return 0;
- }
- }
-
- protected int readISOString(StringBuffer buffer, InputStream input, int max)
- throws IOException, ID3ReaderException {
-
- int bytesRead = 0;
- char c;
- while (++bytesRead <= max && (c = (char) input.read()) > 0) {
- if (buffer != null) {
- buffer.append(c);
- }
- }
- return bytesRead;
- }
-
- private int readUnicodeString(StringBuffer strBuffer, InputStream input, int max, Charset charset)
- throws IOException, ID3ReaderException {
- byte[] buffer = new byte[max];
- int c, cZero = -1;
- int i = 0;
- for (; i < max; i++) {
- c = input.read();
- if (c == -1) {
- break;
- } else if (c == 0) {
- if (cZero == 0) {
- // termination character found
- break;
- } else {
- cZero = 0;
- }
- } else {
- buffer[i] = (byte) c;
- cZero = -1;
- }
- }
- if (strBuffer != null) {
- strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString());
- }
- return i;
- }
-
- public int onStartTagHeader(TagHeader header) {
- return ACTION_SKIP;
- }
-
- public int onStartFrameHeader(FrameHeader header, InputStream input)
- throws IOException, ID3ReaderException {
- return ACTION_SKIP;
- }
-
- public void onEndTag() {
-
- }
-
- public void onNoTagHeaderFound() {
-
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java b/src/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java
deleted file mode 100644
index c458540ee..000000000
--- a/src/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package de.danoeh.antennapod.util.id3reader;
-
-public class ID3ReaderException extends Exception {
-
- public ID3ReaderException() {
- }
-
- public ID3ReaderException(String arg0) {
- super(arg0);
- }
-
- public ID3ReaderException(Throwable arg0) {
- super(arg0);
- }
-
- public ID3ReaderException(String arg0, Throwable arg1) {
- super(arg0, arg1);
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java b/src/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java
deleted file mode 100644
index df73393a5..000000000
--- a/src/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package de.danoeh.antennapod.util.id3reader.model;
-
-public class FrameHeader extends Header {
-
- protected char flags;
-
- public FrameHeader(String id, int size, char flags) {
- super(id, size);
- this.flags = flags;
- }
-
- @Override
- public String toString() {
- return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size));
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/id3reader/model/Header.java b/src/de/danoeh/antennapod/util/id3reader/model/Header.java
deleted file mode 100644
index 22d5b6376..000000000
--- a/src/de/danoeh/antennapod/util/id3reader/model/Header.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.danoeh.antennapod.util.id3reader.model;
-
-public abstract class Header {
-
- protected String id;
- protected int size;
-
- public Header(String id, int size) {
- super();
- this.id = id;
- this.size = size;
- }
-
- public String getId() {
- return id;
- }
-
- public int getSize() {
- return size;
- }
-
- @Override
- public String toString() {
- return "Header [id=" + id + ", size=" + size + "]";
- }
-
-
-
-}
diff --git a/src/de/danoeh/antennapod/util/id3reader/model/TagHeader.java b/src/de/danoeh/antennapod/util/id3reader/model/TagHeader.java
deleted file mode 100644
index ec99ef14e..000000000
--- a/src/de/danoeh/antennapod/util/id3reader/model/TagHeader.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.danoeh.antennapod.util.id3reader.model;
-
-public class TagHeader extends Header {
-
- protected char version;
- protected byte flags;
-
- public TagHeader(String id, int size, char version, byte flags) {
- super(id, size);
- this.version = version;
- this.flags = flags;
- }
-
- @Override
- public String toString() {
- return "TagHeader [version=" + version + ", flags=" + flags + ", id="
- + id + ", size=" + size + "]";
- }
-
- public char getVersion() {
- return version;
- }
-
-
-
-}
diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
deleted file mode 100644
index 2c7a7f074..000000000
--- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
+++ /dev/null
@@ -1,191 +0,0 @@
-package de.danoeh.antennapod.util.menuhandler;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.util.QueueAccess;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java
deleted file mode 100644
index a3adec66d..000000000
--- a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package de.danoeh.antennapod.util.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.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.FeedInfoActivity;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.service.download.DownloadService;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.DownloadRequestException;
-import de.danoeh.antennapod.storage.DownloadRequester;
-import de.danoeh.antennapod.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/src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java b/src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java
deleted file mode 100644
index 7aa04d24c..000000000
--- a/src/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package de.danoeh.antennapod.util.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.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/src/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java b/src/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java
deleted file mode 100644
index 9c611a452..000000000
--- a/src/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package de.danoeh.antennapod.util.menuhandler;
-
-/**
- * Defines useful methods for activities that have a navigation drawer
- */
-public interface NavDrawerActivity {
-
- public boolean isDrawerOpen();
-}
diff --git a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java b/src/de/danoeh/antennapod/util/playback/AudioPlayer.java
deleted file mode 100644
index bd49b0d18..000000000
--- a/src/de/danoeh/antennapod/util/playback/AudioPlayer.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.SurfaceHolder;
-import com.aocate.media.MediaPlayer;
-
-public class AudioPlayer extends MediaPlayer implements IPlayer {
- private static final String TAG = "AudioPlayer";
-
- public AudioPlayer(Context context) {
- super(context);
- }
-
- @Override
- public void setScreenOnWhilePlaying(boolean screenOn) {
- Log.e(TAG, "Setting screen on while playing not supported in Audio Player");
- throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player");
-
- }
-
- @Override
- public void setDisplay(SurfaceHolder sh) {
- if (sh != null) {
- Log.e(TAG, "Setting display not supported in Audio Player");
- throw new UnsupportedOperationException("Setting display not supported in Audio Player");
- }
- }
-
- @Override
- public void setVideoScalingMode(int mode) {
- throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player");
- }
-}
diff --git a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java
deleted file mode 100644
index 3f6e6ae0a..000000000
--- a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java
+++ /dev/null
@@ -1,237 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.media.MediaMetadataRetriever;
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.util.ChapterUtils;
-
-import java.io.File;
-import java.io.InputStream;
-import java.util.List;
-import java.util.concurrent.Callable;
-
-/** Represents a media file that is stored on the local storage device. */
-public class ExternalMedia implements Playable {
-
- public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2;
- public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl";
- public static final String PREF_POSITION = "ExternalMedia.PrefPosition";
- public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType";
-
- private String source;
-
- private String episodeTitle;
- private String feedTitle;
- private MediaType mediaType = MediaType.AUDIO;
- private List<Chapter> chapters;
- private int duration;
- private int position;
-
- public ExternalMedia(String source, MediaType mediaType) {
- super();
- this.source = source;
- this.mediaType = mediaType;
- }
-
- public ExternalMedia(String source, MediaType mediaType, int position) {
- this(source, mediaType);
- this.position = position;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(source);
- dest.writeString(mediaType.toString());
- dest.writeInt(position);
- }
-
- @Override
- public void writeToPreferences(Editor prefEditor) {
- prefEditor.putString(PREF_SOURCE_URL, source);
- prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString());
- prefEditor.putInt(PREF_POSITION, position);
- }
-
- @Override
- public void loadMetadata() throws PlayableException {
- MediaMetadataRetriever mmr = new MediaMetadataRetriever();
- try {
- mmr.setDataSource(source);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- throw new PlayableException(
- "IllegalArgumentException when setting up MediaMetadataReceiver");
- } catch (RuntimeException e) {
- // http://code.google.com/p/android/issues/detail?id=39770
- e.printStackTrace();
- throw new PlayableException(
- "RuntimeException when setting up MediaMetadataRetriever");
- }
- episodeTitle = mmr
- .extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
- feedTitle = mmr
- .extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
- try {
- duration = Integer.parseInt(mmr
- .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
- } catch (NumberFormatException e) {
- e.printStackTrace();
- throw new PlayableException("NumberFormatException when reading duration of media file");
- }
- ChapterUtils.loadChaptersFromFileUrl(this);
- }
-
- @Override
- public void loadChapterMarks() {
-
- }
-
- @Override
- public String getEpisodeTitle() {
- return episodeTitle;
- }
-
- @Override
- public Callable<String> loadShownotes() {
- return new Callable<String>() {
- @Override
- public String call() throws Exception {
- return "";
- }
- };
- }
-
- @Override
- public List<Chapter> getChapters() {
- return chapters;
- }
-
- @Override
- public String getWebsiteLink() {
- return null;
- }
-
- @Override
- public String getPaymentLink() {
- return null;
- }
-
- @Override
- public String getFeedTitle() {
- return feedTitle;
- }
-
- @Override
- public Object getIdentifier() {
- return source;
- }
-
- @Override
- public int getDuration() {
- return duration;
- }
-
- @Override
- public int getPosition() {
- return position;
- }
-
- @Override
- public MediaType getMediaType() {
- return mediaType;
- }
-
- @Override
- public String getLocalMediaUrl() {
- return source;
- }
-
- @Override
- public String getStreamUrl() {
- return null;
- }
-
- @Override
- public boolean localFileAvailable() {
- return true;
- }
-
- @Override
- public boolean streamAvailable() {
- return false;
- }
-
- @Override
- public void saveCurrentPosition(SharedPreferences pref, int newPosition) {
- SharedPreferences.Editor editor = pref.edit();
- editor.putInt(PREF_POSITION, newPosition);
- position = newPosition;
- editor.commit();
- }
-
- @Override
- public void setPosition(int newPosition) {
- position = newPosition;
- }
-
- @Override
- public void setDuration(int newDuration) {
- duration = newDuration;
- }
-
- @Override
- public void onPlaybackStart() {
-
- }
-
- @Override
- public void onPlaybackCompleted() {
-
- }
-
- @Override
- public int getPlayableType() {
- return PLAYABLE_TYPE_EXTERNAL_MEDIA;
- }
-
- @Override
- public void setChapters(List<Chapter> chapters) {
- this.chapters = chapters;
- }
-
- public static final Parcelable.Creator<ExternalMedia> CREATOR = new Parcelable.Creator<ExternalMedia>() {
- public ExternalMedia createFromParcel(Parcel in) {
- String source = in.readString();
- MediaType type = MediaType.valueOf(in.readString());
- int position = 0;
- if (in.dataAvail() > 0) {
- position = in.readInt();
- }
- ExternalMedia extMedia = new ExternalMedia(source, type, position);
- return extMedia;
- }
-
- public ExternalMedia[] newArray(int size) {
- return new ExternalMedia[size];
- }
- };
-
- @Override
- public Uri getImageUri() {
- if (localFileAvailable()) {
- return new Uri.Builder().scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()).build();
- } else {
- return null;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/playback/IPlayer.java b/src/de/danoeh/antennapod/util/playback/IPlayer.java
deleted file mode 100644
index 2d4551b13..000000000
--- a/src/de/danoeh/antennapod/util/playback/IPlayer.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.content.Context;
-import android.view.SurfaceHolder;
-
-import java.io.IOException;
-
-public interface IPlayer {
- boolean canSetPitch();
-
- boolean canSetSpeed();
-
- float getCurrentPitchStepsAdjustment();
-
- int getCurrentPosition();
-
- float getCurrentSpeedMultiplier();
-
- int getDuration();
-
- float getMaxSpeedMultiplier();
-
- float getMinSpeedMultiplier();
-
- boolean isLooping();
-
- boolean isPlaying();
-
- void pause();
-
- void prepare() throws IllegalStateException, IOException;
-
- void prepareAsync();
-
- void release();
-
- void reset();
-
- void seekTo(int msec);
-
- void setAudioStreamType(int streamtype);
-
- void setScreenOnWhilePlaying(boolean screenOn);
-
- void setDataSource(String path) throws IllegalStateException, IOException,
- IllegalArgumentException, SecurityException;
-
- void setDisplay(SurfaceHolder sh);
-
- void setEnableSpeedAdjustment(boolean enableSpeedAdjustment);
-
- void setLooping(boolean looping);
-
- void setPitchStepsAdjustment(float pitchSteps);
-
- void setPlaybackPitch(float f);
-
- void setPlaybackSpeed(float f);
-
- void setVolume(float left, float right);
-
- void start();
-
- void stop();
-
- public void setVideoScalingMode(int mode);
-
- public void setWakeMode(Context context, int mode);
-}
diff --git a/src/de/danoeh/antennapod/util/playback/MediaPlayerError.java b/src/de/danoeh/antennapod/util/playback/MediaPlayerError.java
deleted file mode 100644
index 23ead581f..000000000
--- a/src/de/danoeh/antennapod/util/playback/MediaPlayerError.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.content.Context;
-import android.media.MediaPlayer;
-import de.danoeh.antennapod.R;
-
-/** Utility class for MediaPlayer errors. */
-public class MediaPlayerError {
-
- /** Get a human-readable string for a specific error code. */
- public static String getErrorString(Context context, int code) {
- int resId;
- switch(code) {
- case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
- resId = R.string.playback_error_server_died;
- break;
- default:
- resId = R.string.playback_error_unknown;
- break;
- }
- return context.getString(resId);
- }
-}
diff --git a/src/de/danoeh/antennapod/util/playback/Playable.java b/src/de/danoeh/antennapod/util/playback/Playable.java
deleted file mode 100644
index 004ae56bb..000000000
--- a/src/de/danoeh/antennapod/util/playback/Playable.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Parcelable;
-import android.util.Log;
-
-import java.util.List;
-
-import de.danoeh.antennapod.asynctask.PicassoImageResource;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.util.ShownotesProvider;
-
-/**
- * Interface for objects that can be played by the PlaybackService.
- */
-public interface Playable extends Parcelable,
- ShownotesProvider, PicassoImageResource {
-
- /**
- * Save information about the playable in a preference so that it can be
- * restored later via PlayableUtils.createInstanceFromPreferences.
- * Implementations must NOT call commit() after they have written the values
- * to the preferences file.
- */
- public void writeToPreferences(SharedPreferences.Editor prefEditor);
-
- /**
- * This method is called from a separate thread by the PlaybackService.
- * Playable objects should load their metadata in this method. This method
- * should execute as quickly as possible and NOT load chapter marks if no
- * local file is available.
- */
- public void loadMetadata() throws PlayableException;
-
- /**
- * This method is called from a separate thread by the PlaybackService.
- * Playable objects should load their chapter marks in this method if no
- * local file was available when loadMetadata() was called.
- */
- public void loadChapterMarks();
-
- /**
- * Returns the title of the episode that this playable represents
- */
- public String getEpisodeTitle();
-
- /**
- * Returns a list of chapter marks or null if this Playable has no chapters.
- */
- public List<Chapter> getChapters();
-
- /**
- * Returns a link to a website that is meant to be shown in a browser
- */
- public String getWebsiteLink();
-
- public String getPaymentLink();
-
- /**
- * Returns the title of the feed this Playable belongs to.
- */
- public String getFeedTitle();
-
- /**
- * Returns a unique identifier, for example a file url or an ID from a
- * database.
- */
- public Object getIdentifier();
-
- /**
- * Return duration of object or 0 if duration is unknown.
- */
- public int getDuration();
-
- /**
- * Return position of object or 0 if position is unknown.
- */
- public int getPosition();
-
- /**
- * Returns the type of media. This method should return the correct value
- * BEFORE loadMetadata() is called.
- */
- public MediaType getMediaType();
-
- /**
- * Returns an url to a local file that can be played or null if this file
- * does not exist.
- */
- public String getLocalMediaUrl();
-
- /**
- * Returns an url to a file that can be streamed by the player or null if
- * this url is not known.
- */
- public String getStreamUrl();
-
- /**
- * Returns true if a local file that can be played is available. getFileUrl
- * MUST return a non-null string if this method returns true.
- */
- public boolean localFileAvailable();
-
- /**
- * Returns true if a streamable file is available. getStreamUrl MUST return
- * a non-null string if this method returns true.
- */
- public boolean streamAvailable();
-
- /**
- * Saves the current position of this object. Implementations can use the
- * provided SharedPreference to save this information and retrieve it later
- * via PlayableUtils.createInstanceFromPreferences.
- */
- public void saveCurrentPosition(SharedPreferences pref, int newPosition);
-
- public void setPosition(int newPosition);
-
- public void setDuration(int newDuration);
-
- /**
- * Is called by the PlaybackService when playback starts.
- */
- public void onPlaybackStart();
-
- /**
- * Is called by the PlaybackService when playback is completed.
- */
- public void onPlaybackCompleted();
-
- /**
- * Returns an integer that must be unique among all Playable classes. The
- * return value is later used by PlayableUtils to determine the type of the
- * Playable object that is restored.
- */
- public int getPlayableType();
-
- public void setChapters(List<Chapter> chapters);
-
- /**
- * Provides utility methods for Playable objects.
- */
- public static class PlayableUtils {
- private static final String TAG = "PlayableUtils";
-
- /**
- * Restores a playable object from a sharedPreferences file. This method might load data from the database,
- * depending on the type of playable that was restored.
- *
- * @param type An integer that represents the type of the Playable object
- * that is restored.
- * @param pref The SharedPreferences file from which the Playable object
- * is restored
- * @return The restored Playable object
- */
- public static Playable createInstanceFromPreferences(Context context, int type,
- SharedPreferences pref) {
- // ADD new Playable types here:
- switch (type) {
- case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
- long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
- if (mediaId != -1) {
- return DBReader.getFeedMedia(context, mediaId);
- }
- break;
- case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
- String source = pref.getString(ExternalMedia.PREF_SOURCE_URL,
- null);
- String mediaType = pref.getString(
- ExternalMedia.PREF_MEDIA_TYPE, null);
- if (source != null && mediaType != null) {
- int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
- return new ExternalMedia(source,
- MediaType.valueOf(mediaType), position);
- }
- break;
- }
- Log.e(TAG, "Could not restore Playable object from preferences");
- return null;
- }
- }
-
- public static class PlayableException extends Exception {
- private static final long serialVersionUID = 1L;
-
- public PlayableException() {
- super();
- }
-
- public PlayableException(String detailMessage, Throwable throwable) {
- super(detailMessage, throwable);
- }
-
- public PlayableException(String detailMessage) {
- super(detailMessage);
- }
-
- public PlayableException(Throwable throwable) {
- super(throwable);
- }
-
- }
-}
diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
deleted file mode 100644
index 64dbf4868..000000000
--- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java
+++ /dev/null
@@ -1,784 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.app.Activity;
-import android.content.*;
-import android.content.res.TypedArray;
-import android.media.MediaPlayer;
-import android.os.AsyncTask;
-import android.os.IBinder;
-import android.preference.PreferenceManager;
-import android.util.Log;
-import android.util.Pair;
-import android.view.SurfaceHolder;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.ImageButton;
-import android.widget.SeekBar;
-import android.widget.TextView;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.feed.MediaType;
-import de.danoeh.antennapod.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.service.playback.PlayerStatus;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.util.playback.Playable.PlayableUtils;
-
-import java.util.concurrent.*;
-
-/**
- * Communicates with the playback service. GUI classes should use this class to
- * control playback instead of communicating with the PlaybackService directly.
- */
-public abstract class PlaybackController {
- private static final String TAG = "PlaybackController";
-
- public static final int INVALID_TIME = -1;
-
- private final Activity activity;
-
- private PlaybackService playbackService;
- private Playable media;
- private PlayerStatus status;
-
- private ScheduledThreadPoolExecutor schedExecutor;
- private static final int SCHED_EX_POOLSIZE = 1;
-
- protected MediaPositionObserver positionObserver;
- protected ScheduledFuture positionObserverFuture;
-
- private boolean mediaInfoLoaded = false;
- private boolean released = false;
-
- /**
- * True if controller should reinit playback service if 'pause' button is
- * pressed.
- */
- private boolean reinitOnPause;
-
- public PlaybackController(Activity activity, boolean reinitOnPause) {
- Validate.notNull(activity);
-
- this.activity = activity;
- this.reinitOnPause = reinitOnPause;
- schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE,
- new ThreadFactory() {
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- }, new RejectedExecutionHandler() {
-
- @Override
- public void rejectedExecution(Runnable r,
- ThreadPoolExecutor executor) {
- Log.w(TAG,
- "Rejected execution of runnable in schedExecutor");
- }
- }
- );
- }
-
- /**
- * Creates a new connection to the playbackService. Should be called in the
- * activity's onResume() method.
- */
- public void init() {
- activity.registerReceiver(statusUpdate, new IntentFilter(
- PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
-
- activity.registerReceiver(notificationReceiver, new IntentFilter(
- PlaybackService.ACTION_PLAYER_NOTIFICATION));
-
- activity.registerReceiver(shutdownReceiver, new IntentFilter(
- PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
-
- if (!released) {
- bindToService();
- } else {
- throw new IllegalStateException(
- "Can't call init() after release() has been called");
- }
- }
-
- /**
- * Should be called if the PlaybackController is no longer needed, for
- * example in the activity's onStop() method.
- */
- public void release() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Releasing PlaybackController");
-
- try {
- activity.unregisterReceiver(statusUpdate);
- } catch (IllegalArgumentException e) {
- // ignore
- }
-
- try {
- activity.unregisterReceiver(notificationReceiver);
- } catch (IllegalArgumentException e) {
- // ignore
- }
-
- try {
- activity.unbindService(mConnection);
- } catch (IllegalArgumentException e) {
- // ignore
- }
-
- try {
- activity.unregisterReceiver(shutdownReceiver);
- } catch (IllegalArgumentException e) {
- // ignore
- }
- cancelPositionObserver();
- schedExecutor.shutdownNow();
- media = null;
- released = true;
-
- }
-
- /**
- * Should be called in the activity's onPause() method.
- */
- public void pause() {
- mediaInfoLoaded = false;
- }
-
- /**
- * Tries to establish a connection to the PlaybackService. If it isn't
- * running, the PlaybackService will be started with the last played media
- * as the arguments of the launch intent.
- */
- private void bindToService() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Trying to connect to service");
- AsyncTask<Void, Void, Intent> intentLoader = new AsyncTask<Void, Void, Intent>() {
- @Override
- protected Intent doInBackground(Void... voids) {
- return getPlayLastPlayedMediaIntent();
- }
-
- @Override
- protected void onPostExecute(Intent serviceIntent) {
- boolean bound = false;
- if (!PlaybackService.started) {
- if (serviceIntent != null) {
- if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service");
- activity.startService(serviceIntent);
- bound = activity.bindService(serviceIntent, mConnection, 0);
- } else {
- status = PlayerStatus.STOPPED;
- setupGUI();
- handleStatus();
- }
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG,
- "PlaybackService is running, trying to connect without start command.");
- bound = activity.bindService(new Intent(activity,
- PlaybackService.class), mConnection, 0);
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Result for service binding: " + bound);
- }
- };
- intentLoader.execute();
- }
-
- /**
- * Returns an intent that starts the PlaybackService and plays the last
- * played media or null if no last played media could be found.
- */
- private Intent getPlayLastPlayedMediaIntent() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Trying to restore last played media");
- SharedPreferences prefs = PreferenceManager
- .getDefaultSharedPreferences(activity.getApplicationContext());
- long currentlyPlayingMedia = PlaybackPreferences
- .getCurrentlyPlayingMedia();
- if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
- Playable media = PlayableUtils.createInstanceFromPreferences(activity,
- (int) currentlyPlayingMedia, prefs);
- if (media != null) {
- Intent serviceIntent = new Intent(activity,
- PlaybackService.class);
- serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
- serviceIntent.putExtra(
- PlaybackService.EXTRA_START_WHEN_PREPARED, false);
- serviceIntent.putExtra(
- PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false);
- boolean fileExists = media.localFileAvailable();
- boolean lastIsStream = PlaybackPreferences
- .getCurrentEpisodeIsStream();
- if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
- DBTasks.notifyMissingFeedMediaFile(
- activity, (FeedMedia) media);
- }
- serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
- lastIsStream || !fileExists);
- return serviceIntent;
- }
- }
- if (BuildConfig.DEBUG)
- Log.d(TAG, "No last played media found");
- return null;
- }
-
- public abstract void setupGUI();
-
- private void setupPositionObserver() {
- if ((positionObserverFuture != null && positionObserverFuture
- .isCancelled())
- || (positionObserverFuture != null && positionObserverFuture
- .isDone()) || positionObserverFuture == null) {
-
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Setting up position observer");
- positionObserver = new MediaPositionObserver();
- positionObserverFuture = schedExecutor.scheduleWithFixedDelay(
- positionObserver, MediaPositionObserver.WAITING_INTERVALL,
- MediaPositionObserver.WAITING_INTERVALL,
- TimeUnit.MILLISECONDS);
- }
- }
-
- private void cancelPositionObserver() {
- if (positionObserverFuture != null) {
- boolean result = positionObserverFuture.cancel(true);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "PositionObserver cancelled. Result: " + result);
- }
- }
-
- public abstract void onPositionObserverUpdate();
-
- private ServiceConnection mConnection = new ServiceConnection() {
- public void onServiceConnected(ComponentName className, IBinder service) {
- playbackService = ((PlaybackService.LocalBinder) service)
- .getService();
- if (!released) {
- queryService();
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Connection to Service established");
- } else {
- Log.i(TAG, "Connection to playback service has been established, but controller has already been released");
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- playbackService = null;
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Disconnected from Service");
-
- }
- };
-
- protected BroadcastReceiver statusUpdate = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Received statusUpdate Intent.");
- if (isConnectedToPlaybackService()) {
- PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo();
- status = info.playerStatus;
- media = info.playable;
- handleStatus();
- } else {
- Log.w(TAG,
- "Couldn't receive status update: playbackService was null");
- bindToService();
- }
- }
- };
-
- protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (isConnectedToPlaybackService()) {
- int type = intent.getIntExtra(
- PlaybackService.EXTRA_NOTIFICATION_TYPE, -1);
- int code = intent.getIntExtra(
- PlaybackService.EXTRA_NOTIFICATION_CODE, -1);
- if (code != -1 && type != -1) {
- switch (type) {
- case PlaybackService.NOTIFICATION_TYPE_ERROR:
- handleError(code);
- break;
- case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE:
- float progress = ((float) code) / 100;
- onBufferUpdate(progress);
- break;
- case PlaybackService.NOTIFICATION_TYPE_RELOAD:
- cancelPositionObserver();
- mediaInfoLoaded = false;
- queryService();
- onReloadNotification(intent.getIntExtra(
- PlaybackService.EXTRA_NOTIFICATION_CODE, -1));
- break;
- case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE:
- onSleepTimerUpdate();
- break;
- case PlaybackService.NOTIFICATION_TYPE_BUFFER_START:
- onBufferStart();
- break;
- case PlaybackService.NOTIFICATION_TYPE_BUFFER_END:
- onBufferEnd();
- break;
- case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END:
- onPlaybackEnd();
- break;
- case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE:
- onPlaybackSpeedChange();
- break;
- }
-
- } else {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Bad arguments. Won't handle intent");
- }
- } else {
- bindToService();
- }
- }
-
- };
-
- private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (isConnectedToPlaybackService()) {
- if (StringUtils.equals(intent.getAction(),
- PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
- release();
- onShutdownNotification();
- }
- }
- }
- };
-
- public abstract void onPlaybackSpeedChange();
-
- public abstract void onShutdownNotification();
-
- /**
- * Called when the currently displayed information should be refreshed.
- */
- public abstract void onReloadNotification(int code);
-
- public abstract void onBufferStart();
-
- public abstract void onBufferEnd();
-
- public abstract void onBufferUpdate(float progress);
-
- public abstract void onSleepTimerUpdate();
-
- public abstract void handleError(int code);
-
- public abstract void onPlaybackEnd();
-
- /**
- * Is called whenever the PlaybackService changes it's status. This method
- * should be used to update the GUI or start/cancel background threads.
- */
- private void handleStatus() {
- final int playResource;
- final int pauseResource;
- final CharSequence playText = activity.getString(R.string.play_label);
- final CharSequence pauseText = activity.getString(R.string.pause_label);
-
- if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) {
- TypedArray res = activity.obtainStyledAttributes(new int[]{
- R.attr.av_play, R.attr.av_pause});
- playResource = res.getResourceId(0, R.drawable.av_play);
- pauseResource = res.getResourceId(1, R.drawable.av_pause);
- res.recycle();
- } else {
- playResource = R.drawable.ic_action_play_over_video;
- pauseResource = R.drawable.ic_action_pause_over_video;
- }
-
- switch (status) {
-
- case ERROR:
- postStatusMsg(R.string.player_error_msg);
- handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN);
- break;
- case PAUSED:
- clearStatusMsg();
- checkMediaInfoLoaded();
- cancelPositionObserver();
- updatePlayButtonAppearance(playResource, playText);
- if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
- setScreenOn(false);
- }
- break;
- case PLAYING:
- clearStatusMsg();
- checkMediaInfoLoaded();
- if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
- onAwaitingVideoSurface();
- setScreenOn(true);
- }
- setupPositionObserver();
- updatePlayButtonAppearance(pauseResource, pauseText);
- break;
- case PREPARING:
- postStatusMsg(R.string.player_preparing_msg);
- checkMediaInfoLoaded();
- if (playbackService != null) {
- if (playbackService.isStartWhenPrepared()) {
- updatePlayButtonAppearance(pauseResource, pauseText);
- } else {
- updatePlayButtonAppearance(playResource, playText);
- }
- }
- break;
- case STOPPED:
- postStatusMsg(R.string.player_stopped_msg);
- break;
- case PREPARED:
- checkMediaInfoLoaded();
- postStatusMsg(R.string.player_ready_msg);
- updatePlayButtonAppearance(playResource, playText);
- break;
- case SEEKING:
- postStatusMsg(R.string.player_seeking_msg);
- break;
- case INITIALIZED:
- checkMediaInfoLoaded();
- clearStatusMsg();
- updatePlayButtonAppearance(playResource, playText);
- break;
- }
- }
-
- private void checkMediaInfoLoaded() {
- mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo());
- }
-
- private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) {
- ImageButton butPlay = getPlayButton();
- butPlay.setImageResource(resource);
- butPlay.setContentDescription(contentDescription);
- }
-
- public abstract ImageButton getPlayButton();
-
- public abstract void postStatusMsg(int msg);
-
- public abstract void clearStatusMsg();
-
- public abstract boolean loadMediaInfo();
-
- public abstract void onAwaitingVideoSurface();
-
- /**
- * Called when connection to playback service has been established or
- * information has to be refreshed
- */
- void queryService() {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Querying service info");
- if (playbackService != null) {
- status = playbackService.getStatus();
- media = playbackService.getPlayable();
- /*
- if (media == null) {
- Log.w(TAG,
- "PlaybackService has no media object. Trying to restore last played media.");
- Intent serviceIntent = getPlayLastPlayedMediaIntent();
- if (serviceIntent != null) {
- activity.startService(serviceIntent);
- }
- }
- */
- onServiceQueried();
-
- setupGUI();
- handleStatus();
- // make sure that new media is loaded if it's available
- mediaInfoLoaded = false;
-
- } else {
- Log.e(TAG,
- "queryService() was called without an existing connection to playbackservice");
- }
- }
-
- public abstract void onServiceQueried();
-
- /**
- * Should be used by classes which implement the OnSeekBarChanged interface.
- */
- public float onSeekBarProgressChanged(SeekBar seekBar, int progress,
- boolean fromUser, TextView txtvPosition) {
- if (fromUser && playbackService != null && media != null) {
- float prog = progress / ((float) seekBar.getMax());
- int duration = media.getDuration();
- txtvPosition.setText(Converter
- .getDurationStringLong((int) (prog * duration)));
- return prog;
- }
- return 0;
-
- }
-
- /**
- * Should be used by classes which implement the OnSeekBarChanged interface.
- */
- public void onSeekBarStartTrackingTouch(SeekBar seekBar) {
- // interrupt position Observer, restart later
- cancelPositionObserver();
- }
-
- /**
- * Should be used by classes which implement the OnSeekBarChanged interface.
- */
- public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) {
- if (playbackService != null) {
- playbackService.seekTo((int) (prog * media.getDuration()));
- setupPositionObserver();
- }
- }
-
- /**
- * Should be implemented by classes that show a video. The default implementation
- * does nothing
- *
- * @param enable True if the screen should be kept on, false otherwise
- */
- protected void setScreenOn(boolean enable) {
-
- }
-
- public OnClickListener newOnPlayButtonClickListener() {
- return new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (playbackService != null) {
- switch (status) {
- case PLAYING:
- playbackService.pause(true, reinitOnPause);
- break;
- case PAUSED:
- case PREPARED:
- playbackService.resume();
- break;
- case PREPARING:
- playbackService.setStartWhenPrepared(!playbackService
- .isStartWhenPrepared());
- if (reinitOnPause
- && playbackService.isStartWhenPrepared() == false) {
- playbackService.reinit();
- }
- break;
- case INITIALIZED:
- playbackService.setStartWhenPrepared(true);
- playbackService.prepare();
- break;
- }
- } else {
- Log.w(TAG,
- "Play/Pause button was pressed, but playbackservice was null!");
- }
- }
-
- };
- }
-
- public OnClickListener newOnRevButtonClickListener() {
- return new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (status == PlayerStatus.PLAYING) {
- playbackService.seekDelta(-UserPreferences.getSeekDeltaMs());
- }
- }
- };
- }
-
- public OnClickListener newOnFFButtonClickListener() {
- return new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (status == PlayerStatus.PLAYING) {
- playbackService.seekDelta(UserPreferences.getSeekDeltaMs());
- }
- }
- };
- }
-
- public boolean serviceAvailable() {
- return playbackService != null;
- }
-
- public int getPosition() {
- if (playbackService != null) {
- return playbackService.getCurrentPosition();
- } else {
- return PlaybackService.INVALID_TIME;
- }
- }
-
- public int getDuration() {
- if (playbackService != null) {
- return playbackService.getDuration();
- } else {
- return PlaybackService.INVALID_TIME;
- }
- }
-
- public Playable getMedia() {
- return media;
- }
-
- public boolean sleepTimerActive() {
- return playbackService != null && playbackService.sleepTimerActive();
- }
-
- public boolean sleepTimerNotActive() {
- return playbackService != null && !playbackService.sleepTimerActive();
- }
-
- public void disableSleepTimer() {
- if (playbackService != null) {
- playbackService.disableSleepTimer();
- }
- }
-
- public long getSleepTimerTimeLeft() {
- if (playbackService != null) {
- return playbackService.getSleepTimerTimeLeft();
- } else {
- return INVALID_TIME;
- }
- }
-
- public void setSleepTimer(long time) {
- if (playbackService != null) {
- playbackService.setSleepTimer(time);
- }
- }
-
- public void seekToChapter(Chapter chapter) {
- if (playbackService != null) {
- playbackService.seekToChapter(chapter);
- }
- }
-
- public void seekTo(int time) {
- if (playbackService != null) {
- playbackService.seekTo(time);
- }
- }
-
- public void setVideoSurface(SurfaceHolder holder) {
- if (playbackService != null) {
- playbackService.setVideoSurface(holder);
- }
- }
-
- public PlayerStatus getStatus() {
- return status;
- }
-
- public boolean canSetPlaybackSpeed() {
- return playbackService != null && playbackService.canSetSpeed();
- }
-
- public void setPlaybackSpeed(float speed) {
- if (playbackService != null) {
- playbackService.setSpeed(speed);
- }
- }
-
- public float getCurrentPlaybackSpeedMultiplier() {
- if (canSetPlaybackSpeed()) {
- return playbackService.getCurrentPlaybackSpeed();
- } else {
- return -1;
- }
- }
-
- public boolean isPlayingVideo() {
- if (playbackService != null) {
- return PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
- }
- return false;
- }
-
- public Pair<Integer, Integer> getVideoSize() {
- if (playbackService != null) {
- return playbackService.getVideoSize();
- } else {
- return null;
- }
- }
-
-
- /**
- * Returns true if PlaybackController can communicate with the playback
- * service.
- */
- public boolean isConnectedToPlaybackService() {
- return playbackService != null;
- }
-
- public void notifyVideoSurfaceAbandoned() {
- if (playbackService != null) {
- playbackService.notifyVideoSurfaceAbandoned();
- }
- }
-
- /**
- * Move service into INITIALIZED state if it's paused to save bandwidth
- */
- public void reinitServiceIfPaused() {
- if (playbackService != null
- && playbackService.isStreaming()
- && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService
- .getStatus() == PlayerStatus.PREPARING && playbackService
- .isStartWhenPrepared() == false))) {
- playbackService.reinit();
- }
- }
-
- /**
- * Refreshes the current position of the media file that is playing.
- */
- public class MediaPositionObserver implements Runnable {
-
- public static final int WAITING_INTERVALL = 1000;
-
- @Override
- public void run() {
- if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) {
- activity.runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- onPositionObserverUpdate();
- }
- });
- }
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/playback/Timeline.java b/src/de/danoeh/antennapod/util/playback/Timeline.java
deleted file mode 100644
index ceed68183..000000000
--- a/src/de/danoeh/antennapod/util/playback/Timeline.java
+++ /dev/null
@@ -1,161 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.util.Log;
-import android.util.TypedValue;
-
-import org.apache.commons.lang3.Validate;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.util.Converter;
-import de.danoeh.antennapod.util.ShownotesProvider;
-
-/**
- * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the
- * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's
- * shownotes.
- * <p/>
- * A timeline object needs a shownotesProvider from which the chapter information is retrieved and shownotes are generated.
- */
-public class Timeline {
- private static final String TAG = "Timeline";
-
- private static final String WEBVIEW_STYLE = "@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; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }";
-
-
- private ShownotesProvider shownotesProvider;
-
-
- private final String colorString;
- private final int pageMargin;
-
- public Timeline(Context context, ShownotesProvider shownotesProvider) {
- if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null");
- this.shownotesProvider = shownotesProvider;
-
- TypedArray res = context
- .getTheme()
- .obtainStyledAttributes(
- new int[]{android.R.attr.textColorPrimary});
- int colorResource = res.getColor(0, 0);
- colorString = String.format("#%06X",
- 0xFFFFFF & colorResource);
- res.recycle();
-
- pageMargin = (int) TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, 8, context.getResources()
- .getDisplayMetrics()
- );
- }
-
- private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))");
- private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"antennapod://timecode/%d\">%s</a>";
- private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b");
-
- /**
- * Applies an app-specific CSS stylesheet and adds timecode links (optional).
- * <p/>
- * This method does NOT change the original shownotes string of the shownotesProvider object and it should
- * also not be changed by the caller.
- *
- * @param addTimecodes True if this method should add timecode links
- * @return The processed HTML string.
- */
- public String processShownotes(final boolean addTimecodes) {
- final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null;
-
- // load shownotes
-
- String shownotes;
- try {
- shownotes = shownotesProvider.loadShownotes().call();
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
- if (shownotes == null) {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string");
- return "";
- }
-
- Document document = Jsoup.parse(shownotes);
-
- // apply style
- String styleStr = String.format(WEBVIEW_STYLE, colorString, "100%", pageMargin,
- pageMargin, pageMargin, pageMargin);
- document.head().appendElement("style").attr("type", "text/css").text(styleStr);
-
- // apply timecode links
- if (addTimecodes) {
- Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX);
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes");
- for (Element element : elementsWithTimeCodes) {
- Matcher matcherLong = TIMECODE_REGEX.matcher(element.text());
- StringBuffer buffer = new StringBuffer();
- while (matcherLong.find()) {
- String h = matcherLong.group(1);
- String group = matcherLong.group(0);
- int time = (h != null) ? Converter.durationStringLongToMs(group) :
- Converter.durationStringShortToMs(group);
-
- String rep;
- if (playable == null || playable.getDuration() > time) {
- rep = String.format(TIMECODE_LINK, time, group);
- } else {
- rep = group;
- }
- matcherLong.appendReplacement(buffer, rep);
- }
- matcherLong.appendTail(buffer);
-
- element.html(buffer.toString());
- }
- }
-
- Log.i(TAG, "Out: " + document.toString());
- return document.toString();
- }
-
-
- /**
- * Returns true if the given link is a timecode link.
- */
- public static boolean isTimecodeLink(String link) {
- return link != null && link.matches(TIMECODE_LINK_REGEX.pattern());
- }
-
- /**
- * Returns the time in milliseconds that is attached to this link or -1
- * if the link is no valid timecode link.
- */
- public static int getTimecodeLinkTime(String link) {
- if (isTimecodeLink(link)) {
- Matcher m = TIMECODE_LINK_REGEX.matcher(link);
-
- try {
- if (m.find()) {
- return Integer.valueOf(m.group(1));
- }
- } catch (NumberFormatException e) {
- e.printStackTrace();
- }
- }
- return -1;
- }
-
-
- public void setShownotesProvider(ShownotesProvider shownotesProvider) {
- Validate.notNull(shownotesProvider);
- this.shownotesProvider = shownotesProvider;
- }
-}
diff --git a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java b/src/de/danoeh/antennapod/util/playback/VideoPlayer.java
deleted file mode 100644
index ea9c692ab..000000000
--- a/src/de/danoeh/antennapod/util/playback/VideoPlayer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package de.danoeh.antennapod.util.playback;
-
-import android.media.MediaPlayer;
-import android.util.Log;
-
-public class VideoPlayer extends MediaPlayer implements IPlayer {
- private static final String TAG = "VideoPlayer";
-
- @Override
- public boolean canSetPitch() {
- return false;
- }
-
- @Override
- public boolean canSetSpeed() {
- return false;
- }
-
- @Override
- public float getCurrentPitchStepsAdjustment() {
- return 1;
- }
-
- @Override
- public float getCurrentSpeedMultiplier() {
- return 1;
- }
-
- @Override
- public float getMaxSpeedMultiplier() {
- return 1;
- }
-
- @Override
- public float getMinSpeedMultiplier() {
- return 1;
- }
-
- @Override
- public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException {
- Log.e(TAG, "Setting enable speed adjustment unsupported in video player");
- throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player");
- }
-
- @Override
- public void setPitchStepsAdjustment(float pitchSteps) {
- Log.e(TAG, "Setting pitch steps adjustment unsupported in video player");
- throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player");
- }
-
- @Override
- public void setPlaybackPitch(float f) {
- Log.e(TAG, "Setting playback pitch unsupported in video player");
- throw new UnsupportedOperationException("Setting playback pitch unsupported in video player");
- }
-
- @Override
- public void setPlaybackSpeed(float f) {
- Log.e(TAG, "Setting playback speed unsupported in video player");
- throw new UnsupportedOperationException("Setting playback speed unsupported in video player");
- }
-
- @Override
- public void setVideoScalingMode(int mode) {
- super.setVideoScalingMode(mode);
- }
-}
diff --git a/src/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java b/src/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java
deleted file mode 100644
index ac38ec876..000000000
--- a/src/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package de.danoeh.antennapod.util.syndication;
-
-import android.net.Uri;
-import org.apache.commons.lang3.StringUtils;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here:
- * <p/>
- * http://www.rssboard.org/rss-autodiscovery
- * <p/>
- * http://blog.whatwg.org/feed-autodiscovery
- */
-public class FeedDiscoverer {
-
- private static final String MIME_RSS = "application/rss+xml";
- private static final String MIME_ATOM = "application/atom+xml";
-
- /**
- * Discovers links to RSS and Atom feeds in the given File which must be a HTML document.
- *
- * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if
- * a title cannot be found).
- */
- public Map<String, String> findLinks(File in, String baseUrl) throws IOException {
- return findLinks(Jsoup.parse(in, null), baseUrl);
- }
-
- /**
- * Discovers links to RSS and Atom feeds in the given File which must be a HTML document.
- *
- * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if
- * a title cannot be found).
- */
- public Map<String, String> findLinks(String in, String baseUrl) throws IOException {
- return findLinks(Jsoup.parse(in), baseUrl);
- }
-
- private Map<String, String> findLinks(Document document, String baseUrl) {
- Map<String, String> res = new LinkedHashMap<String, String>();
- Elements links = document.head().getElementsByTag("link");
- for (Element link : links) {
- String rel = link.attr("rel");
- String href = link.attr("href");
- if (!StringUtils.isEmpty(href) &&
- (rel.equals("alternate") || rel.equals("feed"))) {
- String type = link.attr("type");
- if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) {
- String title = link.attr("title");
- String processedUrl = processURL(baseUrl, href);
- if (processedUrl != null) {
- res.put(processedUrl,
- (StringUtils.isEmpty(title)) ? href : title);
- }
- }
- }
- }
- return res;
- }
-
- private String processURL(String baseUrl, String strUrl) {
- Uri uri = Uri.parse(strUrl);
- if (uri.isRelative()) {
- Uri res = Uri.parse(baseUrl).buildUpon().path(strUrl).build();
- return (res != null) ? res.toString() : null;
- } else {
- return strUrl;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java
deleted file mode 100644
index 767034ed2..000000000
--- a/src/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package de.danoeh.antennapod.util.vorbiscommentreader;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Arrays;
-
-public class OggInputStream extends InputStream {
- private InputStream input;
-
- /** True if OggInputStream is currently inside an Ogg page. */
- private boolean isInPage;
- private long bytesLeft;
-
- public OggInputStream(InputStream input) {
- super();
- isInPage = false;
- this.input = input;
- }
-
- @Override
- public int read() throws IOException {
- if (!isInPage) {
- readOggPage();
- }
-
- if (isInPage && bytesLeft > 0) {
- int result = input.read();
- bytesLeft -= 1;
- if (bytesLeft == 0) {
- isInPage = false;
- }
- return result;
- }
- return -1;
- }
-
- private void readOggPage() throws IOException {
- // find OggS
- int[] buffer = new int[4];
- int c = 0;
- boolean isInOggS = false;
- while ((c = input.read()) != -1) {
- switch (c) {
- case 'O':
- isInOggS = true;
- buffer[0] = c;
- break;
- case 'g':
- if (buffer[1] != c) {
- buffer[1] = c;
- } else {
- buffer[2] = c;
- }
- break;
- case 'S':
- buffer[3] = c;
- break;
- default:
- if (isInOggS) {
- Arrays.fill(buffer, 0);
- isInOggS = false;
- }
- }
- if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g'
- && buffer[3] == 'S') {
- break;
- }
- }
- // read segments
- IOUtils.skipFully(input, 22);
- bytesLeft = 0;
- int numSegments = input.read();
- for (int i = 0; i < numSegments; i++) {
- bytesLeft += input.read();
- }
- isInPage = true;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java
deleted file mode 100644
index b2f149ddd..000000000
--- a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package de.danoeh.antennapod.util.vorbiscommentreader;
-
-import android.util.Log;
-import de.danoeh.antennapod.BuildConfig;
-import de.danoeh.antennapod.feed.Chapter;
-import de.danoeh.antennapod.feed.VorbisCommentChapter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class VorbisCommentChapterReader extends VorbisCommentReader {
- private static final String TAG = "VorbisCommentChapterReader";
-
- private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*";
- private static final String CHAPTER_ATTRIBUTE_TITLE = "name";
- private static final String CHAPTER_ATTRIBUTE_LINK = "url";
-
- private List<Chapter> chapters;
-
- public VorbisCommentChapterReader() {
- }
-
- @Override
- public void onVorbisCommentFound() {
- System.out.println("Vorbis comment found");
- }
-
- @Override
- public void onVorbisCommentHeaderFound(VorbisCommentHeader header) {
- chapters = new ArrayList<Chapter>();
- System.out.println(header.toString());
- }
-
- @Override
- public boolean onContentVectorKey(String content) {
- return content.matches(CHAPTER_KEY);
- }
-
- @Override
- public void onContentVectorValue(String key, String value)
- throws VorbisCommentReaderException {
- if (BuildConfig.DEBUG)
- Log.d(TAG, "Key: " + key + ", value: " + value);
- String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key);
- int id = VorbisCommentChapter.getIDFromKey(key);
- Chapter chapter = getChapterById(id);
- if (attribute == null) {
- if (getChapterById(id) == null) {
- // new chapter
- long start = VorbisCommentChapter.getStartTimeFromValue(value);
- chapter = new VorbisCommentChapter(id);
- chapter.setStart(start);
- chapters.add(chapter);
- } else {
- throw new VorbisCommentReaderException(
- "Found chapter with duplicate ID (" + key + ", "
- + value + ")");
- }
- } else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) {
- if (chapter != null) {
- chapter.setTitle(value);
- }
- } else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) {
- if (chapter != null) {
- chapter.setLink(value);
- }
- }
- }
-
- @Override
- public void onNoVorbisCommentFound() {
- System.out.println("No vorbis comment found");
- }
-
- @Override
- public void onEndOfComment() {
- System.out.println("End of comment");
- for (Chapter c : chapters) {
- System.out.println(c.toString());
- }
- }
-
- @Override
- public void onError(VorbisCommentReaderException exception) {
- exception.printStackTrace();
- }
-
- private Chapter getChapterById(long id) {
- for (Chapter c : chapters) {
- if (((VorbisCommentChapter) c).getVorbisCommentId() == id) {
- return c;
- }
- }
- return null;
- }
-
- public List<Chapter> getChapters() {
- return chapters;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java
deleted file mode 100644
index 8c47393c9..000000000
--- a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.danoeh.antennapod.util.vorbiscommentreader;
-public class VorbisCommentHeader {
- private String vendorString;
- private long userCommentLength;
-
- public VorbisCommentHeader(String vendorString, long userCommentLength) {
- super();
- this.vendorString = vendorString;
- this.userCommentLength = userCommentLength;
- }
-
- @Override
- public String toString() {
- return "VorbisCommentHeader [vendorString=" + vendorString
- + ", userCommentLength=" + userCommentLength + "]";
- }
-
- public String getVendorString() {
- return vendorString;
- }
-
- public long getUserCommentLength() {
- return userCommentLength;
- }
-
-}
diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java
deleted file mode 100644
index 718a4f30f..000000000
--- a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java
+++ /dev/null
@@ -1,194 +0,0 @@
-package de.danoeh.antennapod.util.vorbiscommentreader;
-
-import org.apache.commons.io.EndianUtils;
-import org.apache.commons.io.IOUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.util.Arrays;
-
-
-public abstract class VorbisCommentReader {
- /** Length of first page in an ogg file in bytes. */
- private static final int FIRST_PAGE_LENGTH = 58;
- private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024;
- private static final int PACKET_TYPE_IDENTIFICATION = 1;
- private static final int PACKET_TYPE_COMMENT = 3;
-
- /** Called when Reader finds identification header. */
- public abstract void onVorbisCommentFound();
-
- public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header);
-
- /**
- * Is called every time the Reader finds a content vector. The handler
- * should return true if it wants to handle the content vector.
- */
- public abstract boolean onContentVectorKey(String content);
-
- /**
- * Is called if onContentVectorKey returned true for the key.
- *
- * @throws VorbisCommentReaderException
- */
- public abstract void onContentVectorValue(String key, String value)
- throws VorbisCommentReaderException;
-
- public abstract void onNoVorbisCommentFound();
-
- public abstract void onEndOfComment();
-
- public abstract void onError(VorbisCommentReaderException exception);
-
- public void readInputStream(InputStream input)
- throws VorbisCommentReaderException {
- try {
- // look for identification header
- if (findIdentificationHeader(input)) {
-
- onVorbisCommentFound();
- input = new OggInputStream(input);
- if (findCommentHeader(input)) {
- VorbisCommentHeader commentHeader = readCommentHeader(input);
- if (commentHeader != null) {
- onVorbisCommentHeaderFound(commentHeader);
- for (int i = 0; i < commentHeader
- .getUserCommentLength(); i++) {
- try {
- long vectorLength = EndianUtils
- .readSwappedUnsignedInteger(input);
- String key = readContentVectorKey(input,
- vectorLength).toLowerCase();
- boolean readValue = onContentVectorKey(key);
- if (readValue) {
- String value = readUTF8String(
- input,
- (int) (vectorLength - key.length() - 1));
- onContentVectorValue(key, value);
- } else {
- IOUtils.skipFully(input,
- vectorLength - key.length() - 1);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- onEndOfComment();
- }
-
- } else {
- onError(new VorbisCommentReaderException(
- "No comment header found"));
- }
- } else {
- onNoVorbisCommentFound();
- }
- } catch (IOException e) {
- onError(new VorbisCommentReaderException(e));
- }
- }
-
- private String readUTF8String(InputStream input, long length)
- throws IOException {
- byte[] buffer = new byte[(int) length];
-
- IOUtils.readFully(input, buffer);
- Charset charset = Charset.forName("UTF-8");
- return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString();
- }
-
- /**
- * Looks for an identification header in the first page of the file. If an
- * identification header is found, it will be skipped completely and the
- * method will return true, otherwise false.
- *
- * @throws IOException
- */
- private boolean findIdentificationHeader(InputStream input)
- throws IOException {
- byte[] buffer = new byte[FIRST_PAGE_LENGTH];
- IOUtils.readFully(input, buffer);
- int i;
- for (i = 6; i < buffer.length; i++) {
- if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o'
- && buffer[i - 3] == 'r' && buffer[i - 2] == 'b'
- && buffer[i - 1] == 'i' && buffer[i] == 's'
- && buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) {
- return true;
- }
- }
- return false;
- }
-
- private boolean findCommentHeader(InputStream input) throws IOException {
- char[] buffer = new char["vorbis".length() + 1];
- for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
- char c = (char) input.read();
- int dest = -1;
- switch (c) {
- case PACKET_TYPE_COMMENT:
- dest = 0;
- break;
- case 'v':
- dest = 1;
- break;
- case 'o':
- dest = 2;
- break;
- case 'r':
- dest = 3;
- break;
- case 'b':
- dest = 4;
- break;
- case 'i':
- dest = 5;
- break;
- case 's':
- dest = 6;
- break;
- }
- if (dest >= 0) {
- buffer[dest] = c;
- if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r'
- && buffer[4] == 'b' && buffer[5] == 'i'
- && buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) {
- return true;
- }
- } else {
- Arrays.fill(buffer, (char) 0);
- }
- }
- return false;
- }
-
- private VorbisCommentHeader readCommentHeader(InputStream input)
- throws IOException, VorbisCommentReaderException {
- try {
- long vendorLength = EndianUtils.readSwappedUnsignedInteger(input);
- String vendorName = readUTF8String(input, vendorLength);
- long userCommentLength = EndianUtils
- .readSwappedUnsignedInteger(input);
- return new VorbisCommentHeader(vendorName, userCommentLength);
- } catch (UnsupportedEncodingException e) {
- throw new VorbisCommentReaderException(e);
- }
- }
-
- private String readContentVectorKey(InputStream input, long vectorLength)
- throws IOException {
- StringBuffer buffer = new StringBuffer();
- for (int i = 0; i < vectorLength; i++) {
- char c = (char) input.read();
- if (c == '=') {
- return buffer.toString();
- } else {
- buffer.append(c);
- }
- }
- return null; // no key found
- }
-}
diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java
deleted file mode 100644
index 574373241..000000000
--- a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.danoeh.antennapod.util.vorbiscommentreader;
-public class VorbisCommentReaderException extends Exception {
-
- public VorbisCommentReaderException() {
- super();
- // TODO Auto-generated constructor stub
- }
-
- public VorbisCommentReaderException(String arg0, Throwable arg1) {
- super(arg0, arg1);
- // TODO Auto-generated constructor stub
- }
-
- public VorbisCommentReaderException(String arg0) {
- super(arg0);
- // TODO Auto-generated constructor stub
- }
-
- public VorbisCommentReaderException(Throwable arg0) {
- super(arg0);
- // TODO Auto-generated constructor stub
- }
-
-}
diff --git a/src/instrumentationTest/de/test/antennapod/AntennaPodTestRunner.java b/src/instrumentationTest/de/test/antennapod/AntennaPodTestRunner.java
deleted file mode 100644
index cc8494632..000000000
--- a/src/instrumentationTest/de/test/antennapod/AntennaPodTestRunner.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package instrumentationTest.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("instrumentationTest.de.test.antennapod.gpodnet")
- .build();
- }
-}
diff --git a/src/instrumentationTest/de/test/antennapod/gpodnet/GPodnetServiceTest.java b/src/instrumentationTest/de/test/antennapod/gpodnet/GPodnetServiceTest.java
deleted file mode 100644
index a96fc7aab..000000000
--- a/src/instrumentationTest/de/test/antennapod/gpodnet/GPodnetServiceTest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package instrumentationTest.de.test.antennapod.gpodnet;
-
-import android.test.AndroidTestCase;
-import android.util.Log;
-import de.danoeh.antennapod.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
-import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java b/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java
deleted file mode 100644
index 6e44c8a0f..000000000
--- a/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-package instrumentationTest.de.test.antennapod.service.download;
-
-import android.test.InstrumentationTestCase;
-import android.util.Log;
-import de.danoeh.antennapod.feed.FeedFile;
-import de.danoeh.antennapod.service.download.DownloadRequest;
-import de.danoeh.antennapod.service.download.DownloadStatus;
-import de.danoeh.antennapod.service.download.Downloader;
-import de.danoeh.antennapod.service.download.HttpDownloader;
-import de.danoeh.antennapod.util.DownloadError;
-import instrumentationTest.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/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
deleted file mode 100644
index 71747dae8..000000000
--- a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
+++ /dev/null
@@ -1,1177 +0,0 @@
-package instrumentationTest.de.test.antennapod.service.playback;
-
-import android.content.Context;
-import android.media.RemoteControlClient;
-import android.test.InstrumentationTestCase;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.service.playback.PlayerStatus;
-import de.danoeh.antennapod.storage.PodDBAdapter;
-import de.danoeh.antennapod.util.playback.Playable;
-import instrumentationTest.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().getTargetContext().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/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
deleted file mode 100644
index 19f64b4cf..000000000
--- a/src/instrumentationTest/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
+++ /dev/null
@@ -1,333 +0,0 @@
-package instrumentationTest.de.test.antennapod.service.playback;
-
-import android.content.Context;
-import android.test.InstrumentationTestCase;
-import de.danoeh.antennapod.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.service.playback.PlaybackServiceTaskManager;
-import de.danoeh.antennapod.storage.PodDBAdapter;
-import de.danoeh.antennapod.util.playback.Playable;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Test class for PlaybackServiceTaskManager
- */
-public class PlaybackServiceTaskManagerTest extends InstrumentationTestCase {
-
- @Override
- protected void tearDown() throws Exception {
- super.tearDown();
- assertTrue(PodDBAdapter.deleteDatabase(getInstrumentation().getTargetContext()));
- }
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- final Context context = getInstrumentation().getTargetContext();
- context.deleteDatabase(PodDBAdapter.DATABASE_NAME);
- // make sure database is created
- PodDBAdapter adapter = new PodDBAdapter(context);
- adapter.open();
- adapter.close();
- }
-
- public void testInit() {
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(getInstrumentation().getTargetContext(), defaultPSTM);
- pstm.shutdown();
- }
-
- private List<FeedItem> writeTestQueue(String pref) {
- final Context c = getInstrumentation().getTargetContext();
- final int NUM_ITEMS = 10;
- Feed f = new Feed(0, new Date(), "title", "link", "d", null, null, null, null, "id", null, "null", "url", false);
- f.setItems(new ArrayList<FeedItem>());
- for (int i = 0; i < NUM_ITEMS; i++) {
- f.getItems().add(new FeedItem(0, pref + i, pref + i, "link", new Date(), true, f));
- }
- PodDBAdapter adapter = new PodDBAdapter(c);
- adapter.open();
- adapter.setCompleteFeed(f);
- adapter.setQueue(f.getItems());
- adapter.close();
-
- for (FeedItem item : f.getItems()) {
- assertTrue(item.getId() != 0);
- }
- return f.getItems();
- }
-
- public void testGetQueueWriteBeforeCreation() throws InterruptedException {
- final Context c = getInstrumentation().getTargetContext();
- List<FeedItem> queue = writeTestQueue("a");
- assertNotNull(queue);
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- List<FeedItem> testQueue = pstm.getQueue();
- assertNotNull(testQueue);
- assertTrue(queue.size() == testQueue.size());
- for (int i = 0; i < queue.size(); i++) {
- assertTrue(queue.get(i).getId() == testQueue.get(i).getId());
- }
- pstm.shutdown();
- }
-
- public void testGetQueueWriteAfterCreation() throws InterruptedException {
- final Context c = getInstrumentation().getTargetContext();
-
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- List<FeedItem> testQueue = pstm.getQueue();
- assertNotNull(testQueue);
- assertTrue(testQueue.isEmpty());
-
-
- final CountDownLatch countDownLatch = new CountDownLatch(1);
- EventDistributor.EventListener queueListener = new EventDistributor.EventListener() {
- @Override
- public void update(EventDistributor eventDistributor, Integer arg) {
- countDownLatch.countDown();
- }
- };
- EventDistributor.getInstance().register(queueListener);
- List<FeedItem> queue = writeTestQueue("a");
- EventDistributor.getInstance().sendQueueUpdateBroadcast();
- countDownLatch.await(5000, TimeUnit.MILLISECONDS);
-
- assertNotNull(queue);
- testQueue = pstm.getQueue();
- assertNotNull(testQueue);
- assertTrue(queue.size() == testQueue.size());
- for (int i = 0; i < queue.size(); i++) {
- assertTrue(queue.get(i).getId() == testQueue.get(i).getId());
- }
- pstm.shutdown();
- }
-
- public void testStartPositionSaver() throws InterruptedException {
- final Context c = getInstrumentation().getTargetContext();
- final int NUM_COUNTDOWNS = 2;
- final int TIMEOUT = 3 * PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL;
- final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS);
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
- countDownLatch.countDown();
- }
-
- @Override
- public void onSleepTimerExpired() {
-
- }
-
- @Override
- public void onWidgetUpdaterTick() {
-
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
-
- }
- });
- pstm.startPositionSaver();
- countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
- pstm.shutdown();
- }
-
- public void testIsPositionSaverActive() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.startPositionSaver();
- assertTrue(pstm.isPositionSaverActive());
- pstm.shutdown();
- }
-
- public void testCancelPositionSaver() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.startPositionSaver();
- pstm.cancelPositionSaver();
- assertFalse(pstm.isPositionSaverActive());
- pstm.shutdown();
- }
-
- public void testStartWidgetUpdater() throws InterruptedException {
- final Context c = getInstrumentation().getTargetContext();
- final int NUM_COUNTDOWNS = 2;
- final int TIMEOUT = 3 * PlaybackServiceTaskManager.WIDGET_UPDATER_NOTIFICATION_INTERVAL;
- final CountDownLatch countDownLatch = new CountDownLatch(NUM_COUNTDOWNS);
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
-
- }
-
- @Override
- public void onWidgetUpdaterTick() {
- countDownLatch.countDown();
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
-
- }
- });
- pstm.startWidgetUpdater();
- countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
- pstm.shutdown();
- }
-
- public void testIsWidgetUpdaterActive() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.startWidgetUpdater();
- assertTrue(pstm.isWidgetUpdaterActive());
- pstm.shutdown();
- }
-
- public void testCancelWidgetUpdater() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.startWidgetUpdater();
- pstm.cancelWidgetUpdater();
- assertFalse(pstm.isWidgetUpdaterActive());
- pstm.shutdown();
- }
-
- public void testCancelAllTasksNoTasksStarted() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.cancelAllTasks();
- assertFalse(pstm.isPositionSaverActive());
- assertFalse(pstm.isWidgetUpdaterActive());
- assertFalse(pstm.isSleepTimerActive());
- pstm.shutdown();
- }
-
- public void testCancelAllTasksAllTasksStarted() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.startWidgetUpdater();
- pstm.startPositionSaver();
- pstm.setSleepTimer(100000);
- pstm.cancelAllTasks();
- assertFalse(pstm.isPositionSaverActive());
- assertFalse(pstm.isWidgetUpdaterActive());
- assertFalse(pstm.isSleepTimerActive());
- pstm.shutdown();
- }
-
- public void testSetSleepTimer() throws InterruptedException {
- final Context c = getInstrumentation().getTargetContext();
- final long TIME = 2000;
- final long TIMEOUT = 2 * TIME;
- final CountDownLatch countDownLatch = new CountDownLatch(1);
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
- if (countDownLatch.getCount() == 0) {
- fail();
- }
- countDownLatch.countDown();
- }
-
- @Override
- public void onWidgetUpdaterTick() {
-
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
-
- }
- });
- pstm.setSleepTimer(TIME);
- countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
- pstm.shutdown();
- }
-
- public void testDisableSleepTimer() throws InterruptedException {
- final Context c = getInstrumentation().getTargetContext();
- final long TIME = 1000;
- final long TIMEOUT = 2 * TIME;
- final CountDownLatch countDownLatch = new CountDownLatch(1);
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
- fail("Sleeptimer expired");
- }
-
- @Override
- public void onWidgetUpdaterTick() {
-
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
-
- }
- });
- pstm.setSleepTimer(TIME);
- pstm.disableSleepTimer();
- assertFalse(countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS));
- pstm.shutdown();
- }
-
- public void testIsSleepTimerActivePositive() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.setSleepTimer(10000);
- assertTrue(pstm.isSleepTimerActive());
- pstm.shutdown();
- }
-
- public void testIsSleepTimerActiveNegative() {
- final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
- pstm.setSleepTimer(10000);
- pstm.disableSleepTimer();
- assertFalse(pstm.isSleepTimerActive());
- pstm.shutdown();
- }
-
- private final PlaybackServiceTaskManager.PSTMCallback defaultPSTM = new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
-
- }
-
- @Override
- public void onWidgetUpdaterTick() {
-
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
-
- }
- };
-}
diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java
deleted file mode 100644
index c42c7a0cc..000000000
--- a/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java
+++ /dev/null
@@ -1,408 +0,0 @@
-package instrumentationTest.de.test.antennapod.storage;
-
-import android.content.Context;
-import android.test.InstrumentationTestCase;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.FeedItemStatistics;
-import de.danoeh.antennapod.storage.PodDBAdapter;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-import static instrumentationTest.de.test.antennapod.storage.DBTestUtils.*;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Random;
-
-import static instrumentationTest.de.test.antennapod.storage.DBTestUtils.saveFeedlist;
-
-/**
- * Test class for DBReader
- */
-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/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java
deleted file mode 100644
index 1e5afd0e3..000000000
--- a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java
+++ /dev/null
@@ -1,326 +0,0 @@
-package instrumentationTest.de.test.antennapod.storage;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.test.InstrumentationTestCase;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBTasks;
-import de.danoeh.antennapod.storage.PodDBAdapter;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-
-import static instrumentationTest.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/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java b/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java
deleted file mode 100644
index 7e9e1b908..000000000
--- a/src/instrumentationTest/de/test/antennapod/storage/DBTestUtils.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package instrumentationTest.de.test.antennapod.storage;
-
-import android.content.Context;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.storage.PodDBAdapter;
-import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator;
-import de.danoeh.antennapod.util.flattr.FlattrStatus;
-import junit.framework.Assert;
-
-import java.util.ArrayList;
-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/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java
deleted file mode 100644
index 67d99f9fc..000000000
--- a/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java
+++ /dev/null
@@ -1,796 +0,0 @@
-package instrumentationTest.de.test.antennapod.storage;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.test.InstrumentationTestCase;
-import android.util.Log;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedImage;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java b/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java
deleted file mode 100644
index 213dac66a..000000000
--- a/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java
+++ /dev/null
@@ -1,176 +0,0 @@
-package instrumentationTest.de.test.antennapod.syndication.handler;
-
-import android.content.Context;
-import android.test.InstrumentationTestCase;
-import de.danoeh.antennapod.feed.*;
-import de.danoeh.antennapod.syndication.handler.FeedHandler;
-import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException;
-import instrumentationTest.de.test.antennapod.util.syndication.feedgenerator.AtomGenerator;
-import instrumentationTest.de.test.antennapod.util.syndication.feedgenerator.FeedGenerator;
-import instrumentationTest.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/src/instrumentationTest/de/test/antennapod/ui/MainActivityTest.java b/src/instrumentationTest/de/test/antennapod/ui/MainActivityTest.java
deleted file mode 100644
index 23fc224c8..000000000
--- a/src/instrumentationTest/de/test/antennapod/ui/MainActivityTest.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package instrumentationTest.de.test.antennapod.ui;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-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.feed.EventDistributor;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/ui/PlaybackTest.java b/src/instrumentationTest/de/test/antennapod/ui/PlaybackTest.java
deleted file mode 100644
index 98d93a35d..000000000
--- a/src/instrumentationTest/de/test/antennapod/ui/PlaybackTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-package instrumentationTest.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.feed.FeedItem;
-import de.danoeh.antennapod.preferences.UserPreferences;
-import de.danoeh.antennapod.service.playback.PlaybackService;
-import de.danoeh.antennapod.storage.DBReader;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/ui/UITestUtils.java b/src/instrumentationTest/de/test/antennapod/ui/UITestUtils.java
deleted file mode 100644
index 8877d46d6..000000000
--- a/src/instrumentationTest/de/test/antennapod/ui/UITestUtils.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package instrumentationTest.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.feed.*;
-import de.danoeh.antennapod.storage.DBWriter;
-import de.danoeh.antennapod.storage.PodDBAdapter;
-import instrumentationTest.de.test.antennapod.util.service.download.HTTPBin;
-import instrumentationTest.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/src/instrumentationTest/de/test/antennapod/ui/UITestUtilsTest.java b/src/instrumentationTest/de/test/antennapod/ui/UITestUtilsTest.java
deleted file mode 100644
index 88180152c..000000000
--- a/src/instrumentationTest/de/test/antennapod/ui/UITestUtilsTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package instrumentationTest.de.test.antennapod.ui;
-
-import android.test.InstrumentationTestCase;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java b/src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java
deleted file mode 100644
index 807552571..000000000
--- a/src/instrumentationTest/de/test/antennapod/ui/VideoplayerActivityTest.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package instrumentationTest.de.test.antennapod.ui;
-
-import android.test.ActivityInstrumentationTestCase2;
-
-import com.robotium.solo.Solo;
-
-import de.danoeh.antennapod.activity.VideoplayerActivity;
-
-/**
- * Test class for VideoplayerActivity
- */
-public class VideoplayerActivityTest extends ActivityInstrumentationTestCase2<VideoplayerActivity> {
-
- private Solo solo;
-
- public VideoplayerActivityTest() {
- super(VideoplayerActivity.class);
- }
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- solo = new Solo(getInstrumentation(), getActivity());
- }
-
- @Override
- public void tearDown() throws Exception {
- solo.finishOpenedActivities();
- super.tearDown();
- }
-
- /**
- * Test if activity can be started.
- */
- public void testStartActivity() throws Exception {
- solo.waitForActivity(VideoplayerActivity.class);
- }
-}
diff --git a/src/instrumentationTest/de/test/antennapod/util/ConverterTest.java b/src/instrumentationTest/de/test/antennapod/util/ConverterTest.java
deleted file mode 100644
index 8e5674b06..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/ConverterTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package instrumentationTest.de.test.antennapod.util;
-
-import android.test.AndroidTestCase;
-
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/FilenameGeneratorTest.java b/src/instrumentationTest/de/test/antennapod/util/FilenameGeneratorTest.java
deleted file mode 100644
index 552d34941..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/FilenameGeneratorTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package instrumentationTest.de.test.antennapod.util;
-
-import java.io.File;
-import java.io.IOException;
-
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/URIUtilTest.java b/src/instrumentationTest/de/test/antennapod/util/URIUtilTest.java
deleted file mode 100644
index a7cba4c03..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/URIUtilTest.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package instrumentationTest.de.test.antennapod.util;
-
-import android.test.AndroidTestCase;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java b/src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java
deleted file mode 100644
index fa99303b1..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/URLCheckerTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package instrumentationTest.de.test.antennapod.util;
-
-import android.test.AndroidTestCase;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java b/src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java
deleted file mode 100644
index 1b1d011d4..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package instrumentationTest.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.feed.Chapter;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.util.playback.Playable;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java b/src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java
deleted file mode 100644
index fc5025b14..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/service/download/HTTPBin.java
+++ /dev/null
@@ -1,346 +0,0 @@
-package instrumentationTest.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/src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java b/src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java
deleted file mode 100644
index 916a2af6c..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/service/download/NanoHTTPD.java
+++ /dev/null
@@ -1,1420 +0,0 @@
-package instrumentationTest.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/src/instrumentationTest/de/test/antennapod/util/syndication/FeedDiscovererTest.java b/src/instrumentationTest/de/test/antennapod/util/syndication/FeedDiscovererTest.java
deleted file mode 100644
index 51a28089d..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/syndication/FeedDiscovererTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package instrumentationTest.de.test.antennapod.util.syndication;
-
-import android.test.InstrumentationTestCase;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java
deleted file mode 100644
index 0e3440030..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package instrumentationTest.de.test.antennapod.util.syndication.feedgenerator;
-
-import android.util.Xml;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.feed.FeedMedia;
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java
deleted file mode 100644
index e6b0f0697..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package instrumentationTest.de.test.antennapod.util.syndication.feedgenerator;
-
-import de.danoeh.antennapod.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/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java
deleted file mode 100644
index d8bef2354..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package instrumentationTest.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/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java
deleted file mode 100644
index 824a84a66..000000000
--- a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package instrumentationTest.de.test.antennapod.util.syndication.feedgenerator;
-
-import android.util.Xml;
-import de.danoeh.antennapod.feed.Feed;
-import de.danoeh.antennapod.feed.FeedItem;
-import de.danoeh.antennapod.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/submodules/dslv b/submodules/dslv
deleted file mode 160000
-Subproject cde289ca23772bbcd5c72c187011c6cbd22a76c
diff --git a/tests/.classpath b/tests/.classpath
deleted file mode 100644
index 62d428f2b..000000000
--- a/tests/.classpath
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
- <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
- <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
- <classpathentry kind="src" path="src"/>
- <classpathentry kind="src" path="gen"/>
- <classpathentry combineaccessrules="false" kind="src" path="/AntennaPod"/>
- <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
- <classpathentry kind="output" path="bin/classes"/>
-</classpath>
diff --git a/tests/.project b/tests/.project
deleted file mode 100644
index 22c678688..000000000
--- a/tests/.project
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
- <name>AntennaPodTest</name>
- <comment></comment>
- <projects>
- </projects>
- <buildSpec>
- <buildCommand>
- <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- <buildCommand>
- <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- <buildCommand>
- <name>org.eclipse.jdt.core.javabuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- <buildCommand>
- <name>com.android.ide.eclipse.adt.ApkBuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- </buildSpec>
- <natures>
- <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
- <nature>org.eclipse.jdt.core.javanature</nature>
- </natures>
-</projectDescription>
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
deleted file mode 100644
index d2d661e83..000000000
--- a/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="de.danoeh.antennapod.test"
- android:versionCode="1"
- android:versionName="1.0" >
-
- <uses-sdk android:minSdkVersion="10" />
-
- <instrumentation
- android:name="android.test.InstrumentationTestRunner"
- android:targetPackage="de.danoeh.antennapod" />
-
- <application
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name" >
- <uses-library android:name="android.test.runner" />
- </application>
-
-</manifest> \ No newline at end of file
diff --git a/tests/lint.xml b/tests/lint.xml
deleted file mode 100644
index ee0eead5b..000000000
--- a/tests/lint.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<lint>
-</lint> \ No newline at end of file
diff --git a/tests/proguard-project.txt b/tests/proguard-project.txt
deleted file mode 100644
index f2fe1559a..000000000
--- a/tests/proguard-project.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-# To enable ProGuard in your project, edit project.properties
-# to define the proguard.config property as described in that file.
-#
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in ${sdk.dir}/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the ProGuard
-# include property in project.properties.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
diff --git a/tests/project.properties b/tests/project.properties
deleted file mode 100644
index 0840b4a05..000000000
--- a/tests/project.properties
+++ /dev/null
@@ -1,14 +0,0 @@
-# This file is automatically generated by Android Tools.
-# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
-#
-# This file must be checked in Version Control Systems.
-#
-# To customize properties used by the Ant build system edit
-# "ant.properties", and override values to adapt the script to your
-# project structure.
-#
-# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
-#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
-
-# Project target.
-target=android-15
diff --git a/tests/res/drawable-hdpi/ic_launcher.png b/tests/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index 96a442e5b..000000000
--- a/tests/res/drawable-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/res/drawable-ldpi/ic_launcher.png b/tests/res/drawable-ldpi/ic_launcher.png
deleted file mode 100644
index 99238729d..000000000
--- a/tests/res/drawable-ldpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/res/drawable-mdpi/ic_launcher.png b/tests/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index 359047dfa..000000000
--- a/tests/res/drawable-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/res/drawable-xhdpi/ic_launcher.png b/tests/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index 71c6d760f..000000000
--- a/tests/res/drawable-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/res/values/strings.xml b/tests/res/values/strings.xml
deleted file mode 100644
index 8f0a2c82b..000000000
--- a/tests/res/values/strings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <string name="app_name">AntennapodTestTest</string>
-
-</resources> \ No newline at end of file