From 072639b5b22e816df9f78b5cd8a7d4e5379b6aff Mon Sep 17 00:00:00 2001
From: daniel oeh
Date: Wed, 17 Sep 2014 20:51:45 +0200
Subject: Changed project structure
Switched from custom layout to standard gradle project structure
---
app/src/main/AndroidManifest.xml | 351 +++++
.../aocate/presto/service/IDeathCallback_0_8.aidl | 18 +
.../IOnBufferingUpdateListenerCallback_0_8.aidl | 19 +
.../service/IOnCompletionListenerCallback_0_8.aidl | 19 +
.../service/IOnErrorListenerCallback_0_8.aidl | 19 +
.../service/IOnInfoListenerCallback_0_8.aidl | 19 +
...stmentAvailableChangedListenerCallback_0_8.aidl | 19 +
.../service/IOnPreparedListenerCallback_0_8.aidl | 19 +
.../IOnSeekCompleteListenerCallback_0_8.aidl | 19 +
...stmentAvailableChangedListenerCallback_0_8.aidl | 19 +
.../com/aocate/presto/service/IPlayMedia_0_8.aidl | 75 ++
app/src/main/assets/LICENSE.html | 17 +
app/src/main/assets/LICENSE_APACHE_COMMONS.txt | 202 +++
app/src/main/assets/LICENSE_BETTERPICKERS.txt | 13 +
app/src/main/assets/LICENSE_DSLV.txt | 16 +
app/src/main/assets/LICENSE_FLATTR4J.txt | 202 +++
app/src/main/assets/LICENSE_JSOUP.txt | 21 +
app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt | 202 +++
app/src/main/assets/LICENSE_OKHTTP.txt | 11 +
app/src/main/assets/LICENSE_OKIO.txt | 202 +++
app/src/main/assets/LICENSE_PICASSO.txt | 13 +
app/src/main/assets/LICENSE_PRESTO.txt | 13 +
app/src/main/assets/Roboto-Light.ttf | Bin 0 -> 115200 bytes
app/src/main/assets/Roboto.ttf | Bin 0 -> 114976 bytes
app/src/main/assets/about.html | 82 ++
app/src/main/assets/logo.png | Bin 0 -> 58799 bytes
app/src/main/assets/testfile.mp3 | Bin 0 -> 20606 bytes
.../java/com/aocate/media/AndroidMediaPlayer.java | 470 +++++++
.../main/java/com/aocate/media/MediaPlayer.java | 1296 ++++++++++++++++++
.../java/com/aocate/media/MediaPlayerImpl.java | 118 ++
.../com/aocate/media/ServiceBackedMediaPlayer.java | 1170 ++++++++++++++++
.../com/aocate/media/SpeedAdjustmentAlgorithm.java | 31 +
.../main/java/de/danoeh/antennapod/AppConfig.java | 7 +
.../main/java/de/danoeh/antennapod/PodcastApp.java | 47 +
.../danoeh/antennapod/activity/AboutActivity.java | 32 +
.../antennapod/activity/AudioplayerActivity.java | 746 +++++++++++
.../activity/DefaultOnlineFeedViewActivity.java | 248 ++++
.../activity/DirectoryChooserActivity.java | 370 ++++++
.../activity/DownloadAuthenticationActivity.java | 110 ++
.../antennapod/activity/FeedInfoActivity.java | 192 +++
.../antennapod/activity/FlattrAuthActivity.java | 125 ++
.../danoeh/antennapod/activity/MainActivity.java | 432 ++++++
.../antennapod/activity/MediaplayerActivity.java | 525 ++++++++
.../activity/OnlineFeedViewActivity.java | 428 ++++++
.../activity/OpmlFeedChooserActivity.java | 134 ++
.../activity/OpmlImportBaseActivity.java | 90 ++
.../activity/OpmlImportFromIntentActivity.java | 38 +
.../activity/OpmlImportFromPathActivity.java | 172 +++
.../antennapod/activity/OpmlImportHolder.java | 29 +
.../antennapod/activity/PreferenceActivity.java | 513 ++++++++
.../antennapod/activity/StorageErrorActivity.java | 75 ++
.../antennapod/activity/VideoplayerActivity.java | 359 +++++
.../gpoddernet/GpodnetAuthenticationActivity.java | 372 ++++++
.../antennapod/adapter/ActionButtonCallback.java | 8 +
.../antennapod/adapter/ActionButtonUtils.java | 78 ++
.../de/danoeh/antennapod/adapter/AdapterUtils.java | 57 +
.../antennapod/adapter/ChapterListAdapter.java | 180 +++
.../adapter/DefaultActionButtonCallback.java | 57 +
.../antennapod/adapter/DownloadLogAdapter.java | 112 ++
.../adapter/DownloadedEpisodesListAdapter.java | 122 ++
.../antennapod/adapter/DownloadlistAdapter.java | 142 ++
.../adapter/ExternalEpisodesListAdapter.java | 306 +++++
.../antennapod/adapter/FeedItemlistAdapter.java | 220 ++++
.../adapter/FeedItemlistDescriptionAdapter.java | 55 +
.../danoeh/antennapod/adapter/NavListAdapter.java | 229 ++++
.../antennapod/adapter/NewEpisodesListAdapter.java | 170 +++
.../antennapod/adapter/QueueListAdapter.java | 127 ++
.../antennapod/adapter/SearchlistAdapter.java | 110 ++
.../adapter/gpodnet/PodcastListAdapter.java | 64 +
.../antennapod/asynctask/DownloadObserver.java | 177 +++
.../danoeh/antennapod/asynctask/FeedRemover.java | 74 ++
.../antennapod/asynctask/FlattrClickWorker.java | 238 ++++
.../antennapod/asynctask/FlattrStatusFetcher.java | 47 +
.../antennapod/asynctask/FlattrTokenFetcher.java | 95 ++
.../antennapod/asynctask/OpmlExportWorker.java | 114 ++
.../antennapod/asynctask/OpmlFeedQueuer.java | 69 +
.../antennapod/asynctask/OpmlImportWorker.java | 116 ++
.../antennapod/asynctask/PicassoImageResource.java | 37 +
.../antennapod/asynctask/PicassoProvider.java | 152 +++
.../danoeh/antennapod/backup/OpmlBackupAgent.java | 212 +++
.../antennapod/dialog/AuthenticationDialog.java | 89 ++
.../dialog/AutoFlattrPreferenceDialog.java | 107 ++
.../antennapod/dialog/ConfirmationDialog.java | 64 +
.../dialog/DownloadRequestErrorDialogCreator.java | 30 +
.../danoeh/antennapod/dialog/FeedItemDialog.java | 428 ++++++
.../dialog/GpodnetSetHostnameDialog.java | 67 +
.../de/danoeh/antennapod/dialog/TimeDialog.java | 138 ++
.../antennapod/dialog/VariableSpeedDialog.java | 100 ++
.../java/de/danoeh/antennapod/feed/Chapter.java | 55 +
.../danoeh/antennapod/feed/EventDistributor.java | 140 ++
.../main/java/de/danoeh/antennapod/feed/Feed.java | 445 +++++++
.../de/danoeh/antennapod/feed/FeedComponent.java | 66 +
.../java/de/danoeh/antennapod/feed/FeedFile.java | 105 ++
.../java/de/danoeh/antennapod/feed/FeedImage.java | 77 ++
.../java/de/danoeh/antennapod/feed/FeedItem.java | 333 +++++
.../java/de/danoeh/antennapod/feed/FeedMedia.java | 411 ++++++
.../de/danoeh/antennapod/feed/FeedPreferences.java | 89 ++
.../java/de/danoeh/antennapod/feed/ID3Chapter.java | 36 +
.../java/de/danoeh/antennapod/feed/MediaType.java | 5 +
.../de/danoeh/antennapod/feed/SearchResult.java | 34 +
.../de/danoeh/antennapod/feed/SimpleChapter.java | 25 +
.../antennapod/feed/VorbisCommentChapter.java | 109 ++
.../antennapod/fragment/AddFeedFragment.java | 76 ++
.../fragment/CompletedDownloadsFragment.java | 196 +++
.../danoeh/antennapod/fragment/CoverFragment.java | 105 ++
.../antennapod/fragment/DownloadLogFragment.java | 121 ++
.../antennapod/fragment/DownloadsFragment.java | 145 ++
.../fragment/ExternalPlayerFragment.java | 238 ++++
.../fragment/ItemDescriptionFragment.java | 476 +++++++
.../antennapod/fragment/ItemlistFragment.java | 456 +++++++
.../antennapod/fragment/NewEpisodesFragment.java | 425 ++++++
.../fragment/PlaybackHistoryFragment.java | 288 ++++
.../danoeh/antennapod/fragment/QueueFragment.java | 383 ++++++
.../fragment/RunningDownloadsFragment.java | 69 +
.../danoeh/antennapod/fragment/SearchFragment.java | 258 ++++
.../fragment/gpodnet/GpodnetMainFragment.java | 131 ++
.../fragment/gpodnet/PodcastListFragment.java | 169 +++
.../fragment/gpodnet/PodcastTopListFragment.java | 20 +
.../fragment/gpodnet/SearchListFragment.java | 80 ++
.../fragment/gpodnet/SuggestionListFragment.java | 26 +
.../antennapod/fragment/gpodnet/TagFragment.java | 50 +
.../fragment/gpodnet/TagListFragment.java | 146 ++
.../antennapod/gpoddernet/GpodnetService.java | 718 ++++++++++
.../GpodnetServiceAuthenticationException.java | 21 +
.../GpodnetServiceBadStatusCodeException.java | 12 +
.../gpoddernet/GpodnetServiceException.java | 19 +
.../antennapod/gpoddernet/model/GpodnetDevice.java | 72 +
.../gpoddernet/model/GpodnetPodcast.java | 65 +
.../model/GpodnetSubscriptionChange.java | 41 +
.../antennapod/gpoddernet/model/GpodnetTag.java | 46 +
.../model/GpodnetUploadChangesResponse.java | 56 +
.../de/danoeh/antennapod/opml/OpmlElement.java | 46 +
.../java/de/danoeh/antennapod/opml/OpmlReader.java | 87 ++
.../de/danoeh/antennapod/opml/OpmlSymbols.java | 21 +
.../java/de/danoeh/antennapod/opml/OpmlWriter.java | 65 +
.../antennapod/preferences/GpodnetPreferences.java | 246 ++++
.../preferences/PlaybackPreferences.java | 146 ++
.../antennapod/preferences/UserPreferences.java | 577 ++++++++
.../antennapod/receiver/AlarmUpdateReceiver.java | 33 +
.../receiver/ConnectivityActionReceiver.java | 46 +
.../antennapod/receiver/FeedUpdateReceiver.java | 46 +
.../antennapod/receiver/MediaButtonReceiver.java | 32 +
.../danoeh/antennapod/receiver/PlayerWidget.java | 50 +
.../de/danoeh/antennapod/receiver/SPAReceiver.java | 55 +
.../antennapod/service/GpodnetSyncService.java | 245 ++++
.../service/download/APRedirectHandler.java | 54 +
.../service/download/AntennapodHttpClient.java | 96 ++
.../service/download/DownloadRequest.java | 209 +++
.../service/download/DownloadService.java | 1230 +++++++++++++++++
.../service/download/DownloadStatus.java | 181 +++
.../antennapod/service/download/Downloader.java | 69 +
.../service/download/DownloaderCallback.java | 10 +
.../service/download/HttpDownloader.java | 246 ++++
.../service/playback/PlaybackService.java | 1080 +++++++++++++++
.../playback/PlaybackServiceMediaPlayer.java | 979 ++++++++++++++
.../playback/PlaybackServiceTaskManager.java | 384 ++++++
.../antennapod/service/playback/PlayerStatus.java | 14 +
.../service/playback/PlayerWidgetService.java | 190 +++
.../java/de/danoeh/antennapod/spa/SPAUtil.java | 69 +
.../de/danoeh/antennapod/storage/DBReader.java | 908 +++++++++++++
.../java/de/danoeh/antennapod/storage/DBTasks.java | 895 +++++++++++++
.../de/danoeh/antennapod/storage/DBWriter.java | 974 ++++++++++++++
.../storage/DownloadRequestException.java | 25 +
.../antennapod/storage/DownloadRequester.java | 367 ++++++
.../antennapod/storage/FeedItemStatistics.java | 70 +
.../de/danoeh/antennapod/storage/FeedSearcher.java | 57 +
.../de/danoeh/antennapod/storage/PodDBAdapter.java | 1391 ++++++++++++++++++++
.../syndication/handler/FeedHandler.java | 34 +
.../syndication/handler/FeedHandlerResult.java | 19 +
.../syndication/handler/HandlerState.java | 98 ++
.../syndication/handler/SyndHandler.java | 126 ++
.../antennapod/syndication/handler/TypeGetter.java | 112 ++
.../handler/UnsupportedFeedtypeException.java | 38 +
.../syndication/namespace/NSContent.java | 25 +
.../antennapod/syndication/namespace/NSITunes.java | 51 +
.../antennapod/syndication/namespace/NSMedia.java | 68 +
.../antennapod/syndication/namespace/NSRSS20.java | 141 ++
.../syndication/namespace/NSSimpleChapters.java | 42 +
.../syndication/namespace/Namespace.java | 21 +
.../syndication/namespace/SyndElement.java | 22 +
.../syndication/namespace/atom/AtomText.java | 46 +
.../syndication/namespace/atom/NSAtom.java | 194 +++
.../antennapod/syndication/util/SyndDateUtils.java | 153 +++
.../antennapod/syndication/util/SyndTypeUtils.java | 42 +
.../de/danoeh/antennapod/util/ChapterUtils.java | 261 ++++
.../java/de/danoeh/antennapod/util/Converter.java | 103 ++
.../de/danoeh/antennapod/util/DownloadError.java | 52 +
.../java/de/danoeh/antennapod/util/DuckType.java | 117 ++
.../de/danoeh/antennapod/util/EpisodeFilter.java | 49 +
.../antennapod/util/FeedtitleComparator.java | 15 +
.../danoeh/antennapod/util/FileNameGenerator.java | 36 +
.../antennapod/util/InvalidFeedException.java | 21 +
.../java/de/danoeh/antennapod/util/LangUtils.java | 120 ++
.../de/danoeh/antennapod/util/NetworkUtils.java | 69 +
.../de/danoeh/antennapod/util/QueueAccess.java | 93 ++
.../java/de/danoeh/antennapod/util/ShareUtils.java | 34 +
.../danoeh/antennapod/util/ShownotesProvider.java | 16 +
.../de/danoeh/antennapod/util/StorageUtils.java | 66 +
.../java/de/danoeh/antennapod/util/ThemeUtils.java | 22 +
.../java/de/danoeh/antennapod/util/URIUtil.java | 35 +
.../java/de/danoeh/antennapod/util/URLChecker.java | 51 +
.../danoeh/antennapod/util/UndoBarController.java | 137 ++
.../comparator/ChapterStartTimeComparator.java | 20 +
.../util/comparator/DownloadStatusComparator.java | 15 +
.../util/comparator/FeedItemPubdateComparator.java | 19 +
.../PlaybackCompletionDateComparator.java | 19 +
.../comparator/SearchResultValueComparator.java | 14 +
.../util/exception/MediaFileNotFoundException.java | 23 +
.../util/flattr/FlattrServiceCreator.java | 25 +
.../antennapod/util/flattr/FlattrStatus.java | 68 +
.../danoeh/antennapod/util/flattr/FlattrThing.java | 7 +
.../danoeh/antennapod/util/flattr/FlattrUtils.java | 305 +++++
.../antennapod/util/flattr/SimpleFlattrThing.java | 30 +
.../antennapod/util/gui/FeedItemUndoToken.java | 55 +
.../antennapod/util/id3reader/ChapterReader.java | 118 ++
.../antennapod/util/id3reader/ID3Reader.java | 250 ++++
.../util/id3reader/ID3ReaderException.java | 20 +
.../util/id3reader/model/FrameHeader.java | 17 +
.../antennapod/util/id3reader/model/Header.java | 29 +
.../antennapod/util/id3reader/model/TagHeader.java | 26 +
.../util/menuhandler/FeedItemMenuHandler.java | 191 +++
.../util/menuhandler/FeedMenuHandler.java | 87 ++
.../antennapod/util/menuhandler/MenuItemUtils.java | 31 +
.../util/menuhandler/NavDrawerActivity.java | 9 +
.../antennapod/util/playback/AudioPlayer.java | 34 +
.../antennapod/util/playback/ExternalMedia.java | 237 ++++
.../danoeh/antennapod/util/playback/IPlayer.java | 69 +
.../antennapod/util/playback/MediaPlayerError.java | 23 +
.../danoeh/antennapod/util/playback/Playable.java | 207 +++
.../util/playback/PlaybackController.java | 784 +++++++++++
.../danoeh/antennapod/util/playback/Timeline.java | 161 +++
.../antennapod/util/playback/VideoPlayer.java | 67 +
.../util/syndication/FeedDiscoverer.java | 78 ++
.../util/vorbiscommentreader/OggInputStream.java | 81 ++
.../VorbisCommentChapterReader.java | 101 ++
.../vorbiscommentreader/VorbisCommentHeader.java | 26 +
.../vorbiscommentreader/VorbisCommentReader.java | 194 +++
.../VorbisCommentReaderException.java | 24 +
.../antennapod/view/AspectRatioVideoView.java | 97 ++
app/src/main/res/anim/fade_in.xml | 9 +
app/src/main/res/anim/fade_out.xml | 10 +
.../main/res/drawable-hdpi-v11/ic_stat_antenna.png | Bin 0 -> 678 bytes
.../drawable-hdpi-v11/ic_stat_authentication.png | Bin 0 -> 467 bytes
.../res/drawable-hdpi-v11/stat_notify_sync.png | Bin 0 -> 1012 bytes
.../drawable-hdpi-v11/stat_notify_sync_error.png | Bin 0 -> 1103 bytes
app/src/main/res/drawable-hdpi/action_about.png | Bin 0 -> 1764 bytes
.../main/res/drawable-hdpi/action_about_dark.png | Bin 0 -> 1629 bytes
app/src/main/res/drawable-hdpi/action_search.png | Bin 0 -> 1759 bytes
.../main/res/drawable-hdpi/action_search_dark.png | Bin 0 -> 1764 bytes
app/src/main/res/drawable-hdpi/action_settings.png | Bin 0 -> 1505 bytes
.../res/drawable-hdpi/action_settings_dark.png | Bin 0 -> 1540 bytes
app/src/main/res/drawable-hdpi/action_stream.png | Bin 0 -> 803 bytes
.../main/res/drawable-hdpi/action_stream_dark.png | Bin 0 -> 693 bytes
app/src/main/res/drawable-hdpi/av_download.png | Bin 0 -> 1328 bytes
.../main/res/drawable-hdpi/av_download_dark.png | Bin 0 -> 1331 bytes
app/src/main/res/drawable-hdpi/av_fast_forward.png | Bin 0 -> 1416 bytes
.../res/drawable-hdpi/av_fast_forward_dark.png | Bin 0 -> 1366 bytes
app/src/main/res/drawable-hdpi/av_pause.png | Bin 0 -> 1116 bytes
app/src/main/res/drawable-hdpi/av_pause_dark.png | Bin 0 -> 1114 bytes
app/src/main/res/drawable-hdpi/av_play.png | Bin 0 -> 1405 bytes
app/src/main/res/drawable-hdpi/av_play_dark.png | Bin 0 -> 1410 bytes
app/src/main/res/drawable-hdpi/av_rewind.png | Bin 0 -> 1426 bytes
app/src/main/res/drawable-hdpi/av_rewind_dark.png | Bin 0 -> 1449 bytes
app/src/main/res/drawable-hdpi/content_discard.png | Bin 0 -> 1624 bytes
.../res/drawable-hdpi/content_discard_dark.png | Bin 0 -> 1611 bytes
app/src/main/res/drawable-hdpi/content_new.png | Bin 0 -> 1157 bytes
.../main/res/drawable-hdpi/content_new_dark.png | Bin 0 -> 1142 bytes
app/src/main/res/drawable-hdpi/default_cover.png | Bin 0 -> 1404 bytes
.../main/res/drawable-hdpi/default_cover_dark.png | Bin 0 -> 1426 bytes
.../main/res/drawable-hdpi/device_access_time.png | Bin 0 -> 1875 bytes
.../res/drawable-hdpi/device_access_time_dark.png | Bin 0 -> 1794 bytes
.../main/res/drawable-hdpi/ic_action_overflow.png | Bin 0 -> 225 bytes
.../res/drawable-hdpi/ic_action_overflow_dark.png | Bin 0 -> 217 bytes
.../drawable-hdpi/ic_action_pause_over_video.png | Bin 0 -> 6552 bytes
.../drawable-hdpi/ic_action_play_over_video.png | Bin 0 -> 7123 bytes
app/src/main/res/drawable-hdpi/ic_drag_handle.png | Bin 0 -> 220 bytes
.../main/res/drawable-hdpi/ic_drag_handle_dark.png | Bin 0 -> 204 bytes
app/src/main/res/drawable-hdpi/ic_drawer.png | Bin 0 -> 2829 bytes
app/src/main/res/drawable-hdpi/ic_drawer_dark.png | Bin 0 -> 2826 bytes
app/src/main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 3955 bytes
app/src/main/res/drawable-hdpi/ic_new.png | Bin 0 -> 891 bytes
app/src/main/res/drawable-hdpi/ic_new_dark.png | Bin 0 -> 716 bytes
app/src/main/res/drawable-hdpi/ic_stat_antenna.png | Bin 0 -> 649 bytes
.../res/drawable-hdpi/ic_stat_authentication.png | Bin 0 -> 648 bytes
.../main/res/drawable-hdpi/location_web_site.png | Bin 0 -> 2529 bytes
.../res/drawable-hdpi/location_web_site_dark.png | Bin 0 -> 2516 bytes
.../main/res/drawable-hdpi/navigation_accept.png | Bin 0 -> 1320 bytes
.../res/drawable-hdpi/navigation_accept_dark.png | Bin 0 -> 1335 bytes
.../main/res/drawable-hdpi/navigation_cancel.png | Bin 0 -> 1358 bytes
.../res/drawable-hdpi/navigation_cancel_dark.png | Bin 0 -> 1285 bytes
.../main/res/drawable-hdpi/navigation_chapters.png | Bin 0 -> 1979 bytes
.../res/drawable-hdpi/navigation_chapters_dark.png | Bin 0 -> 1821 bytes
.../main/res/drawable-hdpi/navigation_collapse.png | Bin 0 -> 1425 bytes
.../res/drawable-hdpi/navigation_collapse_dark.png | Bin 0 -> 1384 bytes
.../main/res/drawable-hdpi/navigation_expand.png | Bin 0 -> 1444 bytes
.../res/drawable-hdpi/navigation_expand_dark.png | Bin 0 -> 1405 bytes
.../main/res/drawable-hdpi/navigation_refresh.png | Bin 0 -> 3171 bytes
.../res/drawable-hdpi/navigation_refresh_dark.png | Bin 0 -> 3138 bytes
.../res/drawable-hdpi/navigation_shownotes.png | Bin 0 -> 1363 bytes
.../drawable-hdpi/navigation_shownotes_dark.png | Bin 0 -> 1386 bytes
app/src/main/res/drawable-hdpi/navigation_up.png | Bin 0 -> 2270 bytes
.../main/res/drawable-hdpi/navigation_up_dark.png | Bin 0 -> 2221 bytes
app/src/main/res/drawable-hdpi/social_share.png | Bin 0 -> 1695 bytes
.../main/res/drawable-hdpi/social_share_dark.png | Bin 0 -> 1606 bytes
.../main/res/drawable-hdpi/spinner_button.9.png | Bin 0 -> 318 bytes
.../res/drawable-hdpi/spinner_button_dark.9.png | Bin 0 -> 316 bytes
.../main/res/drawable-hdpi/stat_notify_sync.png | Bin 0 -> 674 bytes
.../res/drawable-hdpi/stat_notify_sync_error.png | Bin 0 -> 708 bytes
app/src/main/res/drawable-hdpi/stat_playlist.png | Bin 0 -> 412 bytes
.../main/res/drawable-hdpi/stat_playlist_dark.png | Bin 0 -> 338 bytes
app/src/main/res/drawable-hdpi/type_audio.png | Bin 0 -> 1983 bytes
app/src/main/res/drawable-hdpi/type_audio_dark.png | Bin 0 -> 2008 bytes
app/src/main/res/drawable-hdpi/type_video.png | Bin 0 -> 1215 bytes
app/src/main/res/drawable-hdpi/type_video_dark.png | Bin 0 -> 1211 bytes
.../main/res/drawable-ldpi-v11/ic_stat_antenna.png | Bin 0 -> 307 bytes
app/src/main/res/drawable-ldpi/action_stream.png | Bin 0 -> 367 bytes
.../main/res/drawable-ldpi/action_stream_dark.png | Bin 0 -> 307 bytes
app/src/main/res/drawable-ldpi/ic_launcher.png | Bin 0 -> 1658 bytes
app/src/main/res/drawable-ldpi/ic_stat_antenna.png | Bin 0 -> 271 bytes
app/src/main/res/drawable-ldpi/stat_playlist.png | Bin 0 -> 239 bytes
.../main/res/drawable-ldpi/stat_playlist_dark.png | Bin 0 -> 219 bytes
.../main/res/drawable-mdpi-v11/ic_stat_antenna.png | Bin 0 -> 414 bytes
.../drawable-mdpi-v11/ic_stat_authentication.png | Bin 0 -> 293 bytes
.../res/drawable-mdpi-v11/stat_notify_sync.png | Bin 0 -> 732 bytes
.../drawable-mdpi-v11/stat_notify_sync_error.png | Bin 0 -> 746 bytes
app/src/main/res/drawable-mdpi/action_about.png | Bin 0 -> 1441 bytes
.../main/res/drawable-mdpi/action_about_dark.png | Bin 0 -> 1333 bytes
app/src/main/res/drawable-mdpi/action_search.png | Bin 0 -> 1429 bytes
.../main/res/drawable-mdpi/action_search_dark.png | Bin 0 -> 1394 bytes
app/src/main/res/drawable-mdpi/action_settings.png | Bin 0 -> 1358 bytes
.../res/drawable-mdpi/action_settings_dark.png | Bin 0 -> 1339 bytes
app/src/main/res/drawable-mdpi/action_stream.png | Bin 0 -> 506 bytes
.../main/res/drawable-mdpi/action_stream_dark.png | Bin 0 -> 426 bytes
app/src/main/res/drawable-mdpi/av_download.png | Bin 0 -> 1230 bytes
.../main/res/drawable-mdpi/av_download_dark.png | Bin 0 -> 1238 bytes
app/src/main/res/drawable-mdpi/av_fast_forward.png | Bin 0 -> 1277 bytes
.../res/drawable-mdpi/av_fast_forward_dark.png | Bin 0 -> 1285 bytes
app/src/main/res/drawable-mdpi/av_pause.png | Bin 0 -> 1109 bytes
app/src/main/res/drawable-mdpi/av_pause_dark.png | Bin 0 -> 1107 bytes
app/src/main/res/drawable-mdpi/av_play.png | Bin 0 -> 1261 bytes
app/src/main/res/drawable-mdpi/av_play_dark.png | Bin 0 -> 1248 bytes
app/src/main/res/drawable-mdpi/av_rewind.png | Bin 0 -> 1277 bytes
app/src/main/res/drawable-mdpi/av_rewind_dark.png | Bin 0 -> 1277 bytes
app/src/main/res/drawable-mdpi/content_discard.png | Bin 0 -> 1359 bytes
.../res/drawable-mdpi/content_discard_dark.png | Bin 0 -> 1358 bytes
app/src/main/res/drawable-mdpi/content_new.png | Bin 0 -> 1099 bytes
.../main/res/drawable-mdpi/content_new_dark.png | Bin 0 -> 1090 bytes
app/src/main/res/drawable-mdpi/default_cover.png | Bin 0 -> 1246 bytes
.../main/res/drawable-mdpi/default_cover_dark.png | Bin 0 -> 1240 bytes
.../main/res/drawable-mdpi/device_access_time.png | Bin 0 -> 1493 bytes
.../res/drawable-mdpi/device_access_time_dark.png | Bin 0 -> 1408 bytes
.../main/res/drawable-mdpi/ic_action_overflow.png | Bin 0 -> 197 bytes
.../res/drawable-mdpi/ic_action_overflow_dark.png | Bin 0 -> 201 bytes
.../drawable-mdpi/ic_action_pause_over_video.png | Bin 0 -> 3233 bytes
.../drawable-mdpi/ic_action_play_over_video.png | Bin 0 -> 3510 bytes
app/src/main/res/drawable-mdpi/ic_drag_handle.png | Bin 0 -> 175 bytes
.../main/res/drawable-mdpi/ic_drag_handle_dark.png | Bin 0 -> 159 bytes
app/src/main/res/drawable-mdpi/ic_drawer.png | Bin 0 -> 2820 bytes
app/src/main/res/drawable-mdpi/ic_drawer_dark.png | Bin 0 -> 2816 bytes
app/src/main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2382 bytes
app/src/main/res/drawable-mdpi/ic_new.png | Bin 0 -> 593 bytes
app/src/main/res/drawable-mdpi/ic_new_dark.png | Bin 0 -> 484 bytes
app/src/main/res/drawable-mdpi/ic_stat_antenna.png | Bin 0 -> 412 bytes
.../res/drawable-mdpi/ic_stat_authentication.png | Bin 0 -> 460 bytes
.../main/res/drawable-mdpi/location_web_site.png | Bin 0 -> 1827 bytes
.../res/drawable-mdpi/location_web_site_dark.png | Bin 0 -> 1842 bytes
.../main/res/drawable-mdpi/navigation_accept.png | Bin 0 -> 1197 bytes
.../res/drawable-mdpi/navigation_accept_dark.png | Bin 0 -> 1191 bytes
.../main/res/drawable-mdpi/navigation_cancel.png | Bin 0 -> 1202 bytes
.../res/drawable-mdpi/navigation_cancel_dark.png | Bin 0 -> 1138 bytes
.../main/res/drawable-mdpi/navigation_chapters.png | Bin 0 -> 1584 bytes
.../res/drawable-mdpi/navigation_chapters_dark.png | Bin 0 -> 1453 bytes
.../main/res/drawable-mdpi/navigation_collapse.png | Bin 0 -> 1238 bytes
.../res/drawable-mdpi/navigation_collapse_dark.png | Bin 0 -> 1208 bytes
.../main/res/drawable-mdpi/navigation_expand.png | Bin 0 -> 1242 bytes
.../res/drawable-mdpi/navigation_expand_dark.png | Bin 0 -> 1214 bytes
.../main/res/drawable-mdpi/navigation_refresh.png | Bin 0 -> 3058 bytes
.../res/drawable-mdpi/navigation_refresh_dark.png | Bin 0 -> 3033 bytes
.../res/drawable-mdpi/navigation_shownotes.png | Bin 0 -> 1254 bytes
.../drawable-mdpi/navigation_shownotes_dark.png | Bin 0 -> 1253 bytes
app/src/main/res/drawable-mdpi/navigation_up.png | Bin 0 -> 2123 bytes
.../main/res/drawable-mdpi/navigation_up_dark.png | Bin 0 -> 2060 bytes
app/src/main/res/drawable-mdpi/social_share.png | Bin 0 -> 1394 bytes
.../main/res/drawable-mdpi/social_share_dark.png | Bin 0 -> 1341 bytes
.../main/res/drawable-mdpi/spinner_button.9.png | Bin 0 -> 266 bytes
.../res/drawable-mdpi/spinner_button_dark.9.png | Bin 0 -> 266 bytes
.../main/res/drawable-mdpi/stat_notify_sync.png | Bin 0 -> 628 bytes
.../res/drawable-mdpi/stat_notify_sync_error.png | Bin 0 -> 627 bytes
app/src/main/res/drawable-mdpi/stat_playlist.png | Bin 0 -> 327 bytes
.../main/res/drawable-mdpi/stat_playlist_dark.png | Bin 0 -> 271 bytes
app/src/main/res/drawable-mdpi/type_audio.png | Bin 0 -> 1580 bytes
app/src/main/res/drawable-mdpi/type_audio_dark.png | Bin 0 -> 1582 bytes
app/src/main/res/drawable-mdpi/type_video.png | Bin 0 -> 1129 bytes
app/src/main/res/drawable-mdpi/type_video_dark.png | Bin 0 -> 1129 bytes
.../res/drawable-xhdpi-v11/ic_stat_antenna.png | Bin 0 -> 1005 bytes
.../drawable-xhdpi-v11/ic_stat_authentication.png | Bin 0 -> 529 bytes
.../res/drawable-xhdpi-v11/stat_notify_sync.png | Bin 0 -> 1306 bytes
.../drawable-xhdpi-v11/stat_notify_sync_error.png | Bin 0 -> 1434 bytes
app/src/main/res/drawable-xhdpi/action_about.png | Bin 0 -> 2257 bytes
.../main/res/drawable-xhdpi/action_about_dark.png | Bin 0 -> 2040 bytes
app/src/main/res/drawable-xhdpi/action_search.png | Bin 0 -> 2117 bytes
.../main/res/drawable-xhdpi/action_search_dark.png | Bin 0 -> 2127 bytes
.../main/res/drawable-xhdpi/action_settings.png | Bin 0 -> 1671 bytes
.../res/drawable-xhdpi/action_settings_dark.png | Bin 0 -> 1641 bytes
app/src/main/res/drawable-xhdpi/action_stream.png | Bin 0 -> 1099 bytes
.../main/res/drawable-xhdpi/action_stream_dark.png | Bin 0 -> 974 bytes
app/src/main/res/drawable-xhdpi/av_download.png | Bin 0 -> 1473 bytes
.../main/res/drawable-xhdpi/av_download_dark.png | Bin 0 -> 1482 bytes
.../main/res/drawable-xhdpi/av_fast_forward.png | Bin 0 -> 1668 bytes
.../res/drawable-xhdpi/av_fast_forward_dark.png | Bin 0 -> 1664 bytes
app/src/main/res/drawable-xhdpi/av_pause.png | Bin 0 -> 1159 bytes
app/src/main/res/drawable-xhdpi/av_pause_dark.png | Bin 0 -> 1181 bytes
app/src/main/res/drawable-xhdpi/av_play.png | Bin 0 -> 1578 bytes
app/src/main/res/drawable-xhdpi/av_play_dark.png | Bin 0 -> 1620 bytes
app/src/main/res/drawable-xhdpi/av_rewind.png | Bin 0 -> 1659 bytes
app/src/main/res/drawable-xhdpi/av_rewind_dark.png | Bin 0 -> 1694 bytes
.../main/res/drawable-xhdpi/content_discard.png | Bin 0 -> 1848 bytes
.../res/drawable-xhdpi/content_discard_dark.png | Bin 0 -> 1824 bytes
app/src/main/res/drawable-xhdpi/content_new.png | Bin 0 -> 1225 bytes
.../main/res/drawable-xhdpi/content_new_dark.png | Bin 0 -> 1221 bytes
app/src/main/res/drawable-xhdpi/content_remove.png | Bin 0 -> 1488 bytes
.../res/drawable-xhdpi/content_remove_dark.png | Bin 0 -> 1348 bytes
app/src/main/res/drawable-xhdpi/default_cover.png | Bin 0 -> 1522 bytes
.../main/res/drawable-xhdpi/default_cover_dark.png | Bin 0 -> 1544 bytes
.../main/res/drawable-xhdpi/device_access_time.png | Bin 0 -> 2423 bytes
.../res/drawable-xhdpi/device_access_time_dark.png | Bin 0 -> 2284 bytes
.../main/res/drawable-xhdpi/ic_action_overflow.png | Bin 0 -> 267 bytes
.../res/drawable-xhdpi/ic_action_overflow_dark.png | Bin 0 -> 262 bytes
.../drawable-xhdpi/ic_action_pause_over_video.png | Bin 0 -> 10241 bytes
.../drawable-xhdpi/ic_action_play_over_video.png | Bin 0 -> 11175 bytes
app/src/main/res/drawable-xhdpi/ic_drag_handle.png | Bin 0 -> 234 bytes
.../res/drawable-xhdpi/ic_drag_handle_dark.png | Bin 0 -> 216 bytes
app/src/main/res/drawable-xhdpi/ic_drawer.png | Bin 0 -> 2836 bytes
app/src/main/res/drawable-xhdpi/ic_drawer_dark.png | Bin 0 -> 1038 bytes
app/src/main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5589 bytes
app/src/main/res/drawable-xhdpi/ic_new.png | Bin 0 -> 1189 bytes
app/src/main/res/drawable-xhdpi/ic_new_dark.png | Bin 0 -> 989 bytes
.../main/res/drawable-xhdpi/ic_stat_antenna.png | Bin 0 -> 942 bytes
.../res/drawable-xhdpi/ic_stat_authentication.png | Bin 0 -> 882 bytes
.../main/res/drawable-xhdpi/ic_undobar_undo.png | Bin 0 -> 1558 bytes
.../main/res/drawable-xhdpi/location_web_site.png | Bin 0 -> 3291 bytes
.../res/drawable-xhdpi/location_web_site_dark.png | Bin 0 -> 3307 bytes
.../main/res/drawable-xhdpi/navigation_accept.png | Bin 0 -> 1546 bytes
.../res/drawable-xhdpi/navigation_accept_dark.png | Bin 0 -> 1599 bytes
.../main/res/drawable-xhdpi/navigation_cancel.png | Bin 0 -> 1488 bytes
.../res/drawable-xhdpi/navigation_cancel_dark.png | Bin 0 -> 1348 bytes
.../res/drawable-xhdpi/navigation_chapters.png | Bin 0 -> 2524 bytes
.../drawable-xhdpi/navigation_chapters_dark.png | Bin 0 -> 2366 bytes
.../res/drawable-xhdpi/navigation_collapse.png | Bin 0 -> 1658 bytes
.../drawable-xhdpi/navigation_collapse_dark.png | Bin 0 -> 1635 bytes
.../main/res/drawable-xhdpi/navigation_expand.png | Bin 0 -> 1702 bytes
.../res/drawable-xhdpi/navigation_expand_dark.png | Bin 0 -> 1677 bytes
.../main/res/drawable-xhdpi/navigation_refresh.png | Bin 0 -> 3272 bytes
.../res/drawable-xhdpi/navigation_refresh_dark.png | Bin 0 -> 3219 bytes
.../res/drawable-xhdpi/navigation_shownotes.png | Bin 0 -> 1414 bytes
.../drawable-xhdpi/navigation_shownotes_dark.png | Bin 0 -> 1446 bytes
app/src/main/res/drawable-xhdpi/navigation_up.png | Bin 0 -> 2471 bytes
.../main/res/drawable-xhdpi/navigation_up_dark.png | Bin 0 -> 2445 bytes
app/src/main/res/drawable-xhdpi/social_share.png | Bin 0 -> 1989 bytes
.../main/res/drawable-xhdpi/social_share_dark.png | Bin 0 -> 1780 bytes
.../main/res/drawable-xhdpi/spinner_button.9.png | Bin 0 -> 405 bytes
.../res/drawable-xhdpi/spinner_button_dark.9.png | Bin 0 -> 406 bytes
app/src/main/res/drawable-xhdpi/stat_playlist.png | Bin 0 -> 494 bytes
.../main/res/drawable-xhdpi/stat_playlist_dark.png | Bin 0 -> 440 bytes
app/src/main/res/drawable-xhdpi/type_audio.png | Bin 0 -> 2437 bytes
.../main/res/drawable-xhdpi/type_audio_dark.png | Bin 0 -> 2489 bytes
app/src/main/res/drawable-xhdpi/type_video.png | Bin 0 -> 1327 bytes
.../main/res/drawable-xhdpi/type_video_dark.png | Bin 0 -> 1337 bytes
app/src/main/res/drawable-xhdpi/undobar.9.png | Bin 0 -> 1665 bytes
.../drawable-xhdpi/undobar_button_focused.9.png | Bin 0 -> 1141 bytes
.../drawable-xhdpi/undobar_button_pressed.9.png | Bin 0 -> 1123 bytes
.../main/res/drawable-xhdpi/undobar_divider.9.png | Bin 0 -> 963 bytes
.../res/drawable-xxhdpi/ic_action_overflow.png | Bin 0 -> 264 bytes
.../drawable-xxhdpi/ic_action_overflow_dark.png | Bin 0 -> 264 bytes
.../drawable-xxhdpi/ic_action_pause_over_video.png | Bin 0 -> 21550 bytes
.../drawable-xxhdpi/ic_action_play_over_video.png | Bin 0 -> 23322 bytes
.../main/res/drawable-xxhdpi/ic_drag_handle.png | Bin 0 -> 290 bytes
.../res/drawable-xxhdpi/ic_drag_handle_dark.png | Bin 0 -> 265 bytes
app/src/main/res/drawable-xxhdpi/ic_drawer.png | Bin 0 -> 202 bytes
.../main/res/drawable-xxhdpi/ic_drawer_dark.png | Bin 0 -> 202 bytes
app/src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 14262 bytes
app/src/main/res/drawable-xxhdpi/ic_new.png | Bin 0 -> 1759 bytes
app/src/main/res/drawable-xxhdpi/ic_new_dark.png | Bin 0 -> 1501 bytes
.../res/drawable-xxhdpi/ic_stat_authentication.png | Bin 0 -> 1266 bytes
app/src/main/res/drawable/badge.xml | 13 +
app/src/main/res/drawable/borderless_button.xml | 13 +
.../main/res/drawable/borderless_button_dark.xml | 13 +
app/src/main/res/drawable/horizontal_divider.9.png | Bin 0 -> 159 bytes
.../drawable/overlay_button_circle_background.xml | 10 +
app/src/main/res/drawable/overlay_drawable.xml | 20 +
.../main/res/drawable/overlay_drawable_dark.xml | 15 +
app/src/main/res/drawable/type_audio.png | Bin 0 -> 1580 bytes
app/src/main/res/drawable/type_video.png | Bin 0 -> 1129 bytes
app/src/main/res/drawable/undobar_button.xml | 22 +
app/src/main/res/drawable/vertical_divider.9.png | Bin 0 -> 191 bytes
app/src/main/res/drawable/white_circle.xml | 11 +
.../main/res/layout-land/audioplayer_activity.xml | 197 +++
.../main/res/layout-land/videoplayer_activity.xml | 84 ++
.../main/res/layout-v14/authentication_dialog.xml | 81 ++
app/src/main/res/layout-v14/directory_chooser.xml | 107 ++
.../download_authentication_activity.xml | 92 ++
app/src/main/res/layout-v14/opml_selection.xml | 61 +
app/src/main/res/layout-v14/time_dialog.xml | 78 ++
app/src/main/res/layout/about.xml | 12 +
app/src/main/res/layout/addfeed.xml | 100 ++
app/src/main/res/layout/audioplayer_activity.xml | 182 +++
app/src/main/res/layout/authentication_dialog.xml | 62 +
.../res/layout/autoflattr_preference_dialog.xml | 35 +
app/src/main/res/layout/cover_fragment.xml | 19 +
app/src/main/res/layout/directory_chooser.xml | 85 ++
.../layout/download_authentication_activity.xml | 69 +
.../res/layout/downloaded_episodeslist_item.xml | 82 ++
app/src/main/res/layout/downloadlist_item.xml | 89 ++
app/src/main/res/layout/downloadlog_item.xml | 71 +
.../main/res/layout/ellipsize_start_listitem.xml | 19 +
app/src/main/res/layout/external_itemlist_item.xml | 115 ++
.../main/res/layout/external_player_fragment.xml | 59 +
app/src/main/res/layout/feedinfo.xml | 204 +++
app/src/main/res/layout/feeditem_dialog.xml | 71 +
app/src/main/res/layout/feeditemlist_header.xml | 65 +
app/src/main/res/layout/feeditemlist_item.xml | 101 ++
app/src/main/res/layout/flattr_auth.xml | 30 +
app/src/main/res/layout/gpodnet_podcast_list.xml | 45 +
.../main/res/layout/gpodnet_podcast_listitem.xml | 45 +
app/src/main/res/layout/gpodnetauth_activity.xml | 10 +
.../main/res/layout/gpodnetauth_credentials.xml | 83 ++
app/src/main/res/layout/gpodnetauth_device.xml | 114 ++
app/src/main/res/layout/gpodnetauth_finish.xml | 42 +
.../main/res/layout/itemdescription_listitem.xml | 27 +
app/src/main/res/layout/listview_activity.xml | 12 +
app/src/main/res/layout/main.xml | 40 +
app/src/main/res/layout/nav_feedlistitem.xml | 39 +
app/src/main/res/layout/nav_listitem.xml | 53 +
app/src/main/res/layout/nav_section_item.xml | 26 +
app/src/main/res/layout/new_episodes_fragment.xml | 43 +
app/src/main/res/layout/new_episodes_listitem.xml | 111 ++
app/src/main/res/layout/onlinefeedview_header.xml | 83 ++
app/src/main/res/layout/opml_import.xml | 27 +
app/src/main/res/layout/opml_selection.xml | 39 +
app/src/main/res/layout/pager_fragment.xml | 12 +
app/src/main/res/layout/player_widget.xml | 52 +
app/src/main/res/layout/queue_fragment.xml | 42 +
app/src/main/res/layout/queue_listitem.xml | 96 ++
app/src/main/res/layout/searchlist_item.xml | 43 +
app/src/main/res/layout/simplechapter_item.xml | 43 +
app/src/main/res/layout/storage_error.xml | 25 +
app/src/main/res/layout/time_dialog.xml | 54 +
app/src/main/res/menu/directory_chooser.xml | 14 +
app/src/main/res/menu/feedinfo.xml | 28 +
app/src/main/res/menu/feeditem.xml | 77 ++
app/src/main/res/menu/feeditem_dialog.xml | 48 +
app/src/main/res/menu/feedlist.xml | 34 +
app/src/main/res/menu/main.xml | 13 +
app/src/main/res/menu/mediaplayer.xml | 40 +
app/src/main/res/menu/new_episodes.xml | 27 +
app/src/main/res/menu/queue_context.xml | 20 +
app/src/main/res/values-az/strings.xml | 217 +++
app/src/main/res/values-ca/strings.xml | 341 +++++
app/src/main/res/values-cs-rCZ/strings.xml | 272 ++++
app/src/main/res/values-da/strings.xml | 329 +++++
app/src/main/res/values-de/strings.xml | 341 +++++
app/src/main/res/values-es-rES/strings.xml | 200 +++
app/src/main/res/values-es/strings.xml | 313 +++++
app/src/main/res/values-fr/strings.xml | 340 +++++
app/src/main/res/values-hi-rIN/strings.xml | 281 ++++
app/src/main/res/values-it-rIT/strings.xml | 289 ++++
app/src/main/res/values-iw-rIL/strings.xml | 305 +++++
app/src/main/res/values-ko/strings.xml | 305 +++++
app/src/main/res/values-land/styles.xml | 6 +
app/src/main/res/values-large/dimens.xml | 8 +
app/src/main/res/values-nl/strings.xml | 305 +++++
app/src/main/res/values-pl-rPL/strings.xml | 330 +++++
app/src/main/res/values-pt-rBR/strings.xml | 280 ++++
app/src/main/res/values-pt/strings.xml | 341 +++++
app/src/main/res/values-ro-rRO/strings.xml | 245 ++++
app/src/main/res/values-ru/strings.xml | 311 +++++
app/src/main/res/values-sv-rSE/strings.xml | 341 +++++
app/src/main/res/values-uk-rUA/strings.xml | 329 +++++
app/src/main/res/values-v11/colors.xml | 5 +
app/src/main/res/values-v14/dimens.xml | 5 +
app/src/main/res/values-v14/styles.xml | 9 +
app/src/main/res/values-v16/styles.xml | 17 +
app/src/main/res/values-v19/colors.xml | 5 +
app/src/main/res/values-zh-rCN/strings.xml | 317 +++++
app/src/main/res/values/arrays.xml | 114 ++
app/src/main/res/values/attrs.xml | 43 +
app/src/main/res/values/colors.xml | 24 +
app/src/main/res/values/dimens.xml | 23 +
app/src/main/res/values/ids.xml | 27 +
app/src/main/res/values/integers.xml | 4 +
app/src/main/res/values/strings.xml | 374 ++++++
app/src/main/res/values/styles.xml | 174 +++
app/src/main/res/xml/player_widget_info.xml | 4 +
app/src/main/res/xml/preferences.xml | 145 ++
app/src/main/res/xml/searchable.xml | 4 +
595 files changed, 49367 insertions(+)
create mode 100644 app/src/main/AndroidManifest.xml
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl
create mode 100644 app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl
create mode 100644 app/src/main/assets/LICENSE.html
create mode 100644 app/src/main/assets/LICENSE_APACHE_COMMONS.txt
create mode 100644 app/src/main/assets/LICENSE_BETTERPICKERS.txt
create mode 100644 app/src/main/assets/LICENSE_DSLV.txt
create mode 100644 app/src/main/assets/LICENSE_FLATTR4J.txt
create mode 100644 app/src/main/assets/LICENSE_JSOUP.txt
create mode 100644 app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt
create mode 100644 app/src/main/assets/LICENSE_OKHTTP.txt
create mode 100644 app/src/main/assets/LICENSE_OKIO.txt
create mode 100644 app/src/main/assets/LICENSE_PICASSO.txt
create mode 100644 app/src/main/assets/LICENSE_PRESTO.txt
create mode 100644 app/src/main/assets/Roboto-Light.ttf
create mode 100644 app/src/main/assets/Roboto.ttf
create mode 100644 app/src/main/assets/about.html
create mode 100755 app/src/main/assets/logo.png
create mode 100644 app/src/main/assets/testfile.mp3
create mode 100644 app/src/main/java/com/aocate/media/AndroidMediaPlayer.java
create mode 100644 app/src/main/java/com/aocate/media/MediaPlayer.java
create mode 100644 app/src/main/java/com/aocate/media/MediaPlayerImpl.java
create mode 100644 app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java
create mode 100644 app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/AppConfig.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/PodcastApp.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/DownloadObserver.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FeedRemover.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/PicassoImageResource.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/PicassoProvider.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/backup/OpmlBackupAgent.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/ConfirmationDialog.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/Chapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/Feed.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedComponent.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedMedia.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedPreferences.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/MediaType.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetService.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlElement.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlSymbols.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/GpodnetPreferences.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/PlaybackPreferences.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/APRedirectHandler.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/AntennapodHttpClient.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloadRequest.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloadService.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloadStatus.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/Downloader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloaderCallback.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/HttpDownloader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackService.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlayerStatus.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DBReader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DBTasks.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DBWriter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DownloadRequestException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DownloadRequester.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/FeedItemStatistics.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/FeedSearcher.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/storage/PodDBAdapter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandler.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/HandlerState.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/SyndHandler.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/TypeGetter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSContent.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSITunes.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSMedia.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSRSS20.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/Namespace.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/SyndElement.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/ChapterUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/Converter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/DownloadError.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/DuckType.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/EpisodeFilter.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/FeedtitleComparator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/FileNameGenerator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/InvalidFeedException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/LangUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/NetworkUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/QueueAccess.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/ShareUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/ShownotesProvider.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/StorageUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/ThemeUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/URIUtil.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/URLChecker.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/UndoBarController.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrStatus.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrThing.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/ChapterReader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3Reader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/model/Header.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/model/TagHeader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/AudioPlayer.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/ExternalMedia.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/IPlayer.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/MediaPlayerError.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/Playable.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/PlaybackController.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/Timeline.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/VideoPlayer.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java
create mode 100644 app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java
create mode 100644 app/src/main/res/anim/fade_in.xml
create mode 100644 app/src/main/res/anim/fade_out.xml
create mode 100644 app/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png
create mode 100755 app/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png
create mode 100644 app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png
create mode 100644 app/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png
create mode 100644 app/src/main/res/drawable-hdpi/action_about.png
create mode 100755 app/src/main/res/drawable-hdpi/action_about_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/action_search.png
create mode 100755 app/src/main/res/drawable-hdpi/action_search_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/action_settings.png
create mode 100755 app/src/main/res/drawable-hdpi/action_settings_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/action_stream.png
create mode 100644 app/src/main/res/drawable-hdpi/action_stream_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/av_download.png
create mode 100755 app/src/main/res/drawable-hdpi/av_download_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/av_fast_forward.png
create mode 100755 app/src/main/res/drawable-hdpi/av_fast_forward_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/av_pause.png
create mode 100755 app/src/main/res/drawable-hdpi/av_pause_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/av_play.png
create mode 100755 app/src/main/res/drawable-hdpi/av_play_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/av_rewind.png
create mode 100755 app/src/main/res/drawable-hdpi/av_rewind_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/content_discard.png
create mode 100755 app/src/main/res/drawable-hdpi/content_discard_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/content_new.png
create mode 100755 app/src/main/res/drawable-hdpi/content_new_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/default_cover.png
create mode 100755 app/src/main/res/drawable-hdpi/default_cover_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/device_access_time.png
create mode 100755 app/src/main/res/drawable-hdpi/device_access_time_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/ic_action_overflow.png
create mode 100644 app/src/main/res/drawable-hdpi/ic_action_overflow_dark.png
create mode 100755 app/src/main/res/drawable-hdpi/ic_action_pause_over_video.png
create mode 100755 app/src/main/res/drawable-hdpi/ic_action_play_over_video.png
create mode 100755 app/src/main/res/drawable-hdpi/ic_drag_handle.png
create mode 100755 app/src/main/res/drawable-hdpi/ic_drag_handle_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/ic_drawer.png
create mode 100644 app/src/main/res/drawable-hdpi/ic_drawer_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png
create mode 100755 app/src/main/res/drawable-hdpi/ic_new.png
create mode 100755 app/src/main/res/drawable-hdpi/ic_new_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/ic_stat_antenna.png
create mode 100755 app/src/main/res/drawable-hdpi/ic_stat_authentication.png
create mode 100644 app/src/main/res/drawable-hdpi/location_web_site.png
create mode 100755 app/src/main/res/drawable-hdpi/location_web_site_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/navigation_accept.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_accept_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/navigation_cancel.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_cancel_dark.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_chapters.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_chapters_dark.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_collapse.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_collapse_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/navigation_expand.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_expand_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/navigation_refresh.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_refresh_dark.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_shownotes.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_shownotes_dark.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_up.png
create mode 100755 app/src/main/res/drawable-hdpi/navigation_up_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/social_share.png
create mode 100755 app/src/main/res/drawable-hdpi/social_share_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/spinner_button.9.png
create mode 100644 app/src/main/res/drawable-hdpi/spinner_button_dark.9.png
create mode 100644 app/src/main/res/drawable-hdpi/stat_notify_sync.png
create mode 100644 app/src/main/res/drawable-hdpi/stat_notify_sync_error.png
create mode 100644 app/src/main/res/drawable-hdpi/stat_playlist.png
create mode 100644 app/src/main/res/drawable-hdpi/stat_playlist_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/type_audio.png
create mode 100755 app/src/main/res/drawable-hdpi/type_audio_dark.png
create mode 100644 app/src/main/res/drawable-hdpi/type_video.png
create mode 100755 app/src/main/res/drawable-hdpi/type_video_dark.png
create mode 100644 app/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png
create mode 100644 app/src/main/res/drawable-ldpi/action_stream.png
create mode 100644 app/src/main/res/drawable-ldpi/action_stream_dark.png
create mode 100644 app/src/main/res/drawable-ldpi/ic_launcher.png
create mode 100644 app/src/main/res/drawable-ldpi/ic_stat_antenna.png
create mode 100644 app/src/main/res/drawable-ldpi/stat_playlist.png
create mode 100644 app/src/main/res/drawable-ldpi/stat_playlist_dark.png
create mode 100644 app/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png
create mode 100755 app/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png
create mode 100644 app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png
create mode 100644 app/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png
create mode 100644 app/src/main/res/drawable-mdpi/action_about.png
create mode 100755 app/src/main/res/drawable-mdpi/action_about_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/action_search.png
create mode 100755 app/src/main/res/drawable-mdpi/action_search_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/action_settings.png
create mode 100755 app/src/main/res/drawable-mdpi/action_settings_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/action_stream.png
create mode 100644 app/src/main/res/drawable-mdpi/action_stream_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/av_download.png
create mode 100755 app/src/main/res/drawable-mdpi/av_download_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/av_fast_forward.png
create mode 100755 app/src/main/res/drawable-mdpi/av_fast_forward_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/av_pause.png
create mode 100755 app/src/main/res/drawable-mdpi/av_pause_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/av_play.png
create mode 100755 app/src/main/res/drawable-mdpi/av_play_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/av_rewind.png
create mode 100755 app/src/main/res/drawable-mdpi/av_rewind_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/content_discard.png
create mode 100755 app/src/main/res/drawable-mdpi/content_discard_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/content_new.png
create mode 100755 app/src/main/res/drawable-mdpi/content_new_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/default_cover.png
create mode 100755 app/src/main/res/drawable-mdpi/default_cover_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/device_access_time.png
create mode 100755 app/src/main/res/drawable-mdpi/device_access_time_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/ic_action_overflow.png
create mode 100644 app/src/main/res/drawable-mdpi/ic_action_overflow_dark.png
create mode 100755 app/src/main/res/drawable-mdpi/ic_action_pause_over_video.png
create mode 100755 app/src/main/res/drawable-mdpi/ic_action_play_over_video.png
create mode 100755 app/src/main/res/drawable-mdpi/ic_drag_handle.png
create mode 100755 app/src/main/res/drawable-mdpi/ic_drag_handle_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/ic_drawer.png
create mode 100644 app/src/main/res/drawable-mdpi/ic_drawer_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png
create mode 100755 app/src/main/res/drawable-mdpi/ic_new.png
create mode 100755 app/src/main/res/drawable-mdpi/ic_new_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/ic_stat_antenna.png
create mode 100755 app/src/main/res/drawable-mdpi/ic_stat_authentication.png
create mode 100644 app/src/main/res/drawable-mdpi/location_web_site.png
create mode 100755 app/src/main/res/drawable-mdpi/location_web_site_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/navigation_accept.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_accept_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/navigation_cancel.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_cancel_dark.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_chapters.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_chapters_dark.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_collapse.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_collapse_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/navigation_expand.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_expand_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/navigation_refresh.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_refresh_dark.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_shownotes.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_shownotes_dark.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_up.png
create mode 100755 app/src/main/res/drawable-mdpi/navigation_up_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/social_share.png
create mode 100755 app/src/main/res/drawable-mdpi/social_share_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/spinner_button.9.png
create mode 100644 app/src/main/res/drawable-mdpi/spinner_button_dark.9.png
create mode 100644 app/src/main/res/drawable-mdpi/stat_notify_sync.png
create mode 100644 app/src/main/res/drawable-mdpi/stat_notify_sync_error.png
create mode 100644 app/src/main/res/drawable-mdpi/stat_playlist.png
create mode 100644 app/src/main/res/drawable-mdpi/stat_playlist_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/type_audio.png
create mode 100755 app/src/main/res/drawable-mdpi/type_audio_dark.png
create mode 100644 app/src/main/res/drawable-mdpi/type_video.png
create mode 100755 app/src/main/res/drawable-mdpi/type_video_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png
create mode 100755 app/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png
create mode 100644 app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png
create mode 100644 app/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png
create mode 100644 app/src/main/res/drawable-xhdpi/action_about.png
create mode 100755 app/src/main/res/drawable-xhdpi/action_about_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/action_search.png
create mode 100755 app/src/main/res/drawable-xhdpi/action_search_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/action_settings.png
create mode 100755 app/src/main/res/drawable-xhdpi/action_settings_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/action_stream.png
create mode 100644 app/src/main/res/drawable-xhdpi/action_stream_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/av_download.png
create mode 100755 app/src/main/res/drawable-xhdpi/av_download_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/av_fast_forward.png
create mode 100755 app/src/main/res/drawable-xhdpi/av_fast_forward_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/av_pause.png
create mode 100755 app/src/main/res/drawable-xhdpi/av_pause_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/av_play.png
create mode 100755 app/src/main/res/drawable-xhdpi/av_play_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/av_rewind.png
create mode 100755 app/src/main/res/drawable-xhdpi/av_rewind_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/content_discard.png
create mode 100755 app/src/main/res/drawable-xhdpi/content_discard_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/content_new.png
create mode 100755 app/src/main/res/drawable-xhdpi/content_new_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/content_remove.png
create mode 100755 app/src/main/res/drawable-xhdpi/content_remove_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/default_cover.png
create mode 100755 app/src/main/res/drawable-xhdpi/default_cover_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/device_access_time.png
create mode 100755 app/src/main/res/drawable-xhdpi/device_access_time_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_overflow.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png
create mode 100755 app/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png
create mode 100755 app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png
create mode 100755 app/src/main/res/drawable-xhdpi/ic_drag_handle.png
create mode 100755 app/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_drawer.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_drawer_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png
create mode 100755 app/src/main/res/drawable-xhdpi/ic_new.png
create mode 100755 app/src/main/res/drawable-xhdpi/ic_new_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_antenna.png
create mode 100755 app/src/main/res/drawable-xhdpi/ic_stat_authentication.png
create mode 100644 app/src/main/res/drawable-xhdpi/ic_undobar_undo.png
create mode 100644 app/src/main/res/drawable-xhdpi/location_web_site.png
create mode 100755 app/src/main/res/drawable-xhdpi/location_web_site_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/navigation_accept.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_accept_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/navigation_cancel.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_cancel_dark.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_chapters.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_chapters_dark.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_collapse.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_collapse_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/navigation_expand.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_expand_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/navigation_refresh.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_refresh_dark.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_shownotes.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_up.png
create mode 100755 app/src/main/res/drawable-xhdpi/navigation_up_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/social_share.png
create mode 100755 app/src/main/res/drawable-xhdpi/social_share_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/spinner_button.9.png
create mode 100644 app/src/main/res/drawable-xhdpi/spinner_button_dark.9.png
create mode 100644 app/src/main/res/drawable-xhdpi/stat_playlist.png
create mode 100644 app/src/main/res/drawable-xhdpi/stat_playlist_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/type_audio.png
create mode 100755 app/src/main/res/drawable-xhdpi/type_audio_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/type_video.png
create mode 100755 app/src/main/res/drawable-xhdpi/type_video_dark.png
create mode 100644 app/src/main/res/drawable-xhdpi/undobar.9.png
create mode 100644 app/src/main/res/drawable-xhdpi/undobar_button_focused.9.png
create mode 100644 app/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png
create mode 100644 app/src/main/res/drawable-xhdpi/undobar_divider.9.png
create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_overflow.png
create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png
create mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png
create mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png
create mode 100755 app/src/main/res/drawable-xxhdpi/ic_drag_handle.png
create mode 100755 app/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png
create mode 100644 app/src/main/res/drawable-xxhdpi/ic_drawer.png
create mode 100644 app/src/main/res/drawable-xxhdpi/ic_drawer_dark.png
create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png
create mode 100755 app/src/main/res/drawable-xxhdpi/ic_new.png
create mode 100755 app/src/main/res/drawable-xxhdpi/ic_new_dark.png
create mode 100755 app/src/main/res/drawable-xxhdpi/ic_stat_authentication.png
create mode 100644 app/src/main/res/drawable/badge.xml
create mode 100644 app/src/main/res/drawable/borderless_button.xml
create mode 100644 app/src/main/res/drawable/borderless_button_dark.xml
create mode 100644 app/src/main/res/drawable/horizontal_divider.9.png
create mode 100644 app/src/main/res/drawable/overlay_button_circle_background.xml
create mode 100644 app/src/main/res/drawable/overlay_drawable.xml
create mode 100644 app/src/main/res/drawable/overlay_drawable_dark.xml
create mode 100644 app/src/main/res/drawable/type_audio.png
create mode 100644 app/src/main/res/drawable/type_video.png
create mode 100644 app/src/main/res/drawable/undobar_button.xml
create mode 100644 app/src/main/res/drawable/vertical_divider.9.png
create mode 100644 app/src/main/res/drawable/white_circle.xml
create mode 100644 app/src/main/res/layout-land/audioplayer_activity.xml
create mode 100644 app/src/main/res/layout-land/videoplayer_activity.xml
create mode 100644 app/src/main/res/layout-v14/authentication_dialog.xml
create mode 100644 app/src/main/res/layout-v14/directory_chooser.xml
create mode 100644 app/src/main/res/layout-v14/download_authentication_activity.xml
create mode 100644 app/src/main/res/layout-v14/opml_selection.xml
create mode 100644 app/src/main/res/layout-v14/time_dialog.xml
create mode 100644 app/src/main/res/layout/about.xml
create mode 100644 app/src/main/res/layout/addfeed.xml
create mode 100644 app/src/main/res/layout/audioplayer_activity.xml
create mode 100644 app/src/main/res/layout/authentication_dialog.xml
create mode 100644 app/src/main/res/layout/autoflattr_preference_dialog.xml
create mode 100644 app/src/main/res/layout/cover_fragment.xml
create mode 100644 app/src/main/res/layout/directory_chooser.xml
create mode 100644 app/src/main/res/layout/download_authentication_activity.xml
create mode 100644 app/src/main/res/layout/downloaded_episodeslist_item.xml
create mode 100644 app/src/main/res/layout/downloadlist_item.xml
create mode 100644 app/src/main/res/layout/downloadlog_item.xml
create mode 100644 app/src/main/res/layout/ellipsize_start_listitem.xml
create mode 100644 app/src/main/res/layout/external_itemlist_item.xml
create mode 100644 app/src/main/res/layout/external_player_fragment.xml
create mode 100644 app/src/main/res/layout/feedinfo.xml
create mode 100644 app/src/main/res/layout/feeditem_dialog.xml
create mode 100644 app/src/main/res/layout/feeditemlist_header.xml
create mode 100644 app/src/main/res/layout/feeditemlist_item.xml
create mode 100644 app/src/main/res/layout/flattr_auth.xml
create mode 100644 app/src/main/res/layout/gpodnet_podcast_list.xml
create mode 100644 app/src/main/res/layout/gpodnet_podcast_listitem.xml
create mode 100644 app/src/main/res/layout/gpodnetauth_activity.xml
create mode 100644 app/src/main/res/layout/gpodnetauth_credentials.xml
create mode 100644 app/src/main/res/layout/gpodnetauth_device.xml
create mode 100644 app/src/main/res/layout/gpodnetauth_finish.xml
create mode 100644 app/src/main/res/layout/itemdescription_listitem.xml
create mode 100644 app/src/main/res/layout/listview_activity.xml
create mode 100644 app/src/main/res/layout/main.xml
create mode 100644 app/src/main/res/layout/nav_feedlistitem.xml
create mode 100644 app/src/main/res/layout/nav_listitem.xml
create mode 100644 app/src/main/res/layout/nav_section_item.xml
create mode 100644 app/src/main/res/layout/new_episodes_fragment.xml
create mode 100644 app/src/main/res/layout/new_episodes_listitem.xml
create mode 100644 app/src/main/res/layout/onlinefeedview_header.xml
create mode 100644 app/src/main/res/layout/opml_import.xml
create mode 100644 app/src/main/res/layout/opml_selection.xml
create mode 100644 app/src/main/res/layout/pager_fragment.xml
create mode 100644 app/src/main/res/layout/player_widget.xml
create mode 100644 app/src/main/res/layout/queue_fragment.xml
create mode 100644 app/src/main/res/layout/queue_listitem.xml
create mode 100644 app/src/main/res/layout/searchlist_item.xml
create mode 100644 app/src/main/res/layout/simplechapter_item.xml
create mode 100644 app/src/main/res/layout/storage_error.xml
create mode 100644 app/src/main/res/layout/time_dialog.xml
create mode 100644 app/src/main/res/menu/directory_chooser.xml
create mode 100644 app/src/main/res/menu/feedinfo.xml
create mode 100644 app/src/main/res/menu/feeditem.xml
create mode 100644 app/src/main/res/menu/feeditem_dialog.xml
create mode 100644 app/src/main/res/menu/feedlist.xml
create mode 100644 app/src/main/res/menu/main.xml
create mode 100644 app/src/main/res/menu/mediaplayer.xml
create mode 100644 app/src/main/res/menu/new_episodes.xml
create mode 100644 app/src/main/res/menu/queue_context.xml
create mode 100644 app/src/main/res/values-az/strings.xml
create mode 100644 app/src/main/res/values-ca/strings.xml
create mode 100644 app/src/main/res/values-cs-rCZ/strings.xml
create mode 100644 app/src/main/res/values-da/strings.xml
create mode 100644 app/src/main/res/values-de/strings.xml
create mode 100644 app/src/main/res/values-es-rES/strings.xml
create mode 100644 app/src/main/res/values-es/strings.xml
create mode 100644 app/src/main/res/values-fr/strings.xml
create mode 100644 app/src/main/res/values-hi-rIN/strings.xml
create mode 100644 app/src/main/res/values-it-rIT/strings.xml
create mode 100644 app/src/main/res/values-iw-rIL/strings.xml
create mode 100644 app/src/main/res/values-ko/strings.xml
create mode 100644 app/src/main/res/values-land/styles.xml
create mode 100644 app/src/main/res/values-large/dimens.xml
create mode 100644 app/src/main/res/values-nl/strings.xml
create mode 100644 app/src/main/res/values-pl-rPL/strings.xml
create mode 100644 app/src/main/res/values-pt-rBR/strings.xml
create mode 100644 app/src/main/res/values-pt/strings.xml
create mode 100644 app/src/main/res/values-ro-rRO/strings.xml
create mode 100644 app/src/main/res/values-ru/strings.xml
create mode 100644 app/src/main/res/values-sv-rSE/strings.xml
create mode 100644 app/src/main/res/values-uk-rUA/strings.xml
create mode 100644 app/src/main/res/values-v11/colors.xml
create mode 100644 app/src/main/res/values-v14/dimens.xml
create mode 100644 app/src/main/res/values-v14/styles.xml
create mode 100644 app/src/main/res/values-v16/styles.xml
create mode 100644 app/src/main/res/values-v19/colors.xml
create mode 100644 app/src/main/res/values-zh-rCN/strings.xml
create mode 100644 app/src/main/res/values/arrays.xml
create mode 100644 app/src/main/res/values/attrs.xml
create mode 100644 app/src/main/res/values/colors.xml
create mode 100644 app/src/main/res/values/dimens.xml
create mode 100644 app/src/main/res/values/ids.xml
create mode 100644 app/src/main/res/values/integers.xml
create mode 100644 app/src/main/res/values/strings.xml
create mode 100644 app/src/main/res/values/styles.xml
create mode 100644 app/src/main/res/xml/player_widget_info.xml
create mode 100644 app/src/main/res/xml/preferences.xml
create mode 100644 app/src/main/res/xml/searchable.xml
(limited to 'app/src/main')
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..65eac99ea
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,351 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl
new file mode 100644
index 000000000..6bdc76801
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl
@@ -0,0 +1,18 @@
+// 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.presto.service;
+
+oneway interface IDeathCallback_0_8 {
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl
new file mode 100644
index 000000000..7357e402e
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnBufferingUpdateListenerCallback_0_8 {
+ void onBufferingUpdate(int percent);
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl
new file mode 100644
index 000000000..d5edea729
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnCompletionListenerCallback_0_8 {
+ void onCompletion();
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl
new file mode 100644
index 000000000..2c4f2df3e
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnErrorListenerCallback_0_8 {
+ boolean onError(int what, int extra);
+}
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl
new file mode 100644
index 000000000..9dbd1d260
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnInfoListenerCallback_0_8 {
+ boolean onInfo(int what, int extra);
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl
new file mode 100644
index 000000000..41223a97b
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 {
+ void onPitchAdjustmentAvailableChanged(boolean pitchAdjustmentAvailable);
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl
new file mode 100644
index 000000000..7be8f1237
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnPreparedListenerCallback_0_8 {
+ void onPrepared();
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl
new file mode 100644
index 000000000..5bdda98b6
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnSeekCompleteListenerCallback_0_8 {
+ void onSeekComplete();
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl
new file mode 100644
index 000000000..a69c1cf34
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl
@@ -0,0 +1,19 @@
+// 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.presto.service;
+
+interface IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 {
+ void onSpeedAdjustmentAvailableChanged(boolean speedAdjustmentAvailable);
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl
new file mode 100644
index 000000000..12a6047de
--- /dev/null
+++ b/app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl
@@ -0,0 +1,75 @@
+// 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.presto.service;
+
+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.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.IOnInfoListenerCallback_0_8;
+
+interface IPlayMedia_0_8 {
+ boolean canSetPitch(long sessionId);
+ boolean canSetSpeed(long sessionId);
+ float getCurrentPitchStepsAdjustment(long sessionId);
+ int getCurrentPosition(long sessionId);
+ float getCurrentSpeedMultiplier(long sessionId);
+ int getDuration(long sessionId);
+ float getMaxSpeedMultiplier(long sessionId);
+ float getMinSpeedMultiplier(long sessionId);
+ int getVersionCode();
+ String getVersionName();
+ boolean isLooping(long sessionId);
+ boolean isPlaying(long sessionId);
+ void pause(long sessionId);
+ void prepare(long sessionId);
+ void prepareAsync(long sessionId);
+ void registerOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb);
+ void registerOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb);
+ void registerOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb);
+ void registerOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb);
+ void registerOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb);
+ void registerOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb);
+ void registerOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb);
+ void registerOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb);
+ void release(long sessionId);
+ void reset(long sessionId);
+ void seekTo(long sessionId, int msec);
+ void setAudioStreamType(long sessionId, int streamtype);
+ void setDataSourceString(long sessionId, String path);
+ void setDataSourceUri(long sessionId, in Uri uri);
+ void setEnableSpeedAdjustment(long sessionId, boolean enableSpeedAdjustment);
+ void setLooping(long sessionId, boolean looping);
+ void setPitchStepsAdjustment(long sessionId, float pitchSteps);
+ void setPlaybackPitch(long sessionId, float f);
+ void setPlaybackSpeed(long sessionId, float f);
+ void setSpeedAdjustmentAlgorithm(long sessionId, int algorithm);
+ void setVolume(long sessionId, float left, float right);
+ void start(long sessionId);
+ long startSession(IDeathCallback_0_8 cb);
+ void stop(long sessionId);
+ void unregisterOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb);
+ void unregisterOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb);
+ void unregisterOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb);
+ void unregisterOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb);
+ void unregisterOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb);
+ void unregisterOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb);
+ void unregisterOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb);
+ void unregisterOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb);
+}
\ No newline at end of file
diff --git a/app/src/main/assets/LICENSE.html b/app/src/main/assets/LICENSE.html
new file mode 100644
index 000000000..d38547791
--- /dev/null
+++ b/app/src/main/assets/LICENSE.html
@@ -0,0 +1,17 @@
+
+
+
+
+ MIT License
+
+
+Copyright (c) 2012 Daniel Oeh
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/LICENSE_APACHE_COMMONS.txt b/app/src/main/assets/LICENSE_APACHE_COMMONS.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_APACHE_COMMONS.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_BETTERPICKERS.txt b/app/src/main/assets/LICENSE_BETTERPICKERS.txt
new file mode 100644
index 000000000..80830ed73
--- /dev/null
+++ b/app/src/main/assets/LICENSE_BETTERPICKERS.txt
@@ -0,0 +1,13 @@
+Copyright 2013 Derek Brameyer
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_DSLV.txt b/app/src/main/assets/LICENSE_DSLV.txt
new file mode 100644
index 000000000..2a2de04a3
--- /dev/null
+++ b/app/src/main/assets/LICENSE_DSLV.txt
@@ -0,0 +1,16 @@
+A subclass of the Android ListView component that enables drag
+and drop re-ordering of list items.
+
+Copyright 2012 Carl Bauer
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_FLATTR4J.txt b/app/src/main/assets/LICENSE_FLATTR4J.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_FLATTR4J.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_JSOUP.txt b/app/src/main/assets/LICENSE_JSOUP.txt
new file mode 100644
index 000000000..f3ef71dbf
--- /dev/null
+++ b/app/src/main/assets/LICENSE_JSOUP.txt
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2009, 2010, 2011, 2012, 2013 Jonathan Hedley
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt b/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_NINE_OLD_ANDROIDS.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_OKHTTP.txt b/app/src/main/assets/LICENSE_OKHTTP.txt
new file mode 100644
index 000000000..90edcee40
--- /dev/null
+++ b/app/src/main/assets/LICENSE_OKHTTP.txt
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_OKIO.txt b/app/src/main/assets/LICENSE_OKIO.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/src/main/assets/LICENSE_OKIO.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/src/main/assets/LICENSE_PICASSO.txt b/app/src/main/assets/LICENSE_PICASSO.txt
new file mode 100644
index 000000000..0bf6b9f8e
--- /dev/null
+++ b/app/src/main/assets/LICENSE_PICASSO.txt
@@ -0,0 +1,13 @@
+Copyright 2013 Square, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/LICENSE_PRESTO.txt b/app/src/main/assets/LICENSE_PRESTO.txt
new file mode 100644
index 000000000..b4b1a8cf5
--- /dev/null
+++ b/app/src/main/assets/LICENSE_PRESTO.txt
@@ -0,0 +1,13 @@
+Copyright 2011, Aocate, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/app/src/main/assets/Roboto-Light.ttf b/app/src/main/assets/Roboto-Light.ttf
new file mode 100644
index 000000000..13bf13af0
Binary files /dev/null and b/app/src/main/assets/Roboto-Light.ttf differ
diff --git a/app/src/main/assets/Roboto.ttf b/app/src/main/assets/Roboto.ttf
new file mode 100644
index 000000000..0ba95c98c
Binary files /dev/null and b/app/src/main/assets/Roboto.ttf differ
diff --git a/app/src/main/assets/about.html b/app/src/main/assets/about.html
new file mode 100644
index 000000000..8b8746add
--- /dev/null
+++ b/app/src/main/assets/about.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ About AntennaPod
+
+
+
+Used libraries
+
+NineOldAndroids (Link)
+by Jake Wharton, licensed under the Apache 2.0 license (View)
+
+Apache Commons (Link)
+by The Apache Software Foundation, licensed under the Apache 2.0 license (View)
+
+
+licensed under the Apache 2.0 license (View)
+
+drag-sort-listview (Link)
+licensed under the Apache 2.0 license (View)
+
+Presto Client (Link)
+licensed under the Apache 2.0 license (View)
+
+Better Pickers (Link)
+licensed under the Apache 2.0 license (View)
+
+
+licensed under the MIT license (View)
+
+
+licensed under the Apache 2.0 license (View)
+
+
+licensed under the Apache 2.0 license (View)
+
+
+licensed under the Apache 2.0 license (View)
+
diff --git a/app/src/main/assets/logo.png b/app/src/main/assets/logo.png
new file mode 100755
index 000000000..d0e988a6d
Binary files /dev/null and b/app/src/main/assets/logo.png differ
diff --git a/app/src/main/assets/testfile.mp3 b/app/src/main/assets/testfile.mp3
new file mode 100644
index 000000000..f15faadf3
Binary files /dev/null and b/app/src/main/assets/testfile.mp3 differ
diff --git a/app/src/main/java/com/aocate/media/AndroidMediaPlayer.java b/app/src/main/java/com/aocate/media/AndroidMediaPlayer.java
new file mode 100644
index 000000000..17ee74a13
--- /dev/null
+++ b/app/src/main/java/com/aocate/media/AndroidMediaPlayer.java
@@ -0,0 +1,470 @@
+// 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 android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.util.Log;
+
+public class AndroidMediaPlayer extends MediaPlayerImpl {
+ private final static String AMP_TAG = "AocateAndroidMediaPlayer";
+
+ // private static final long TIMEOUT_DURATION_MS = 500;
+
+ android.media.MediaPlayer mp = null;
+
+ private android.media.MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() {
+ public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) {
+ if (owningMediaPlayer != null) {
+ owningMediaPlayer.lock.lock();
+ try {
+ if ((owningMediaPlayer.onBufferingUpdateListener != null)
+ && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) {
+ owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ }
+ };
+
+ private android.media.MediaPlayer.OnCompletionListener onCompletionListener = new android.media.MediaPlayer.OnCompletionListener() {
+ public void onCompletion(android.media.MediaPlayer mp) {
+ Log.d(AMP_TAG, "onCompletionListener being called");
+ if (owningMediaPlayer != null) {
+ owningMediaPlayer.lock.lock();
+ try {
+ if (owningMediaPlayer.onCompletionListener != null) {
+ owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ }
+ };
+
+ private android.media.MediaPlayer.OnErrorListener onErrorListener = new android.media.MediaPlayer.OnErrorListener() {
+ public boolean onError(android.media.MediaPlayer mp, int what, int extra) {
+ // Once we're in errored state, any received messages are going to be junked
+ if (owningMediaPlayer != null) {
+ owningMediaPlayer.lock.lock();
+ try {
+ if (owningMediaPlayer.onErrorListener != null) {
+ return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ return false;
+ }
+ };
+
+ private android.media.MediaPlayer.OnInfoListener onInfoListener = new android.media.MediaPlayer.OnInfoListener() {
+ public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) {
+ if (owningMediaPlayer != null) {
+ owningMediaPlayer.lock.lock();
+ try {
+ if ((owningMediaPlayer.onInfoListener != null)
+ && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) {
+ return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ return false;
+ }
+ };
+
+ // We have to assign this.onPreparedListener because the
+ // onPreparedListener in owningMediaPlayer sets the state
+ // to PREPARED. Due to prepareAsync, that's the only
+ // reasonable place to do it
+ // The others it just didn't make sense to have a setOnXListener that didn't use the parameter
+ private android.media.MediaPlayer.OnPreparedListener onPreparedListener = new android.media.MediaPlayer.OnPreparedListener() {
+ public void onPrepared(android.media.MediaPlayer mp) {
+ Log.d(AMP_TAG, "Calling onPreparedListener.onPrepared()");
+ if (AndroidMediaPlayer.this.owningMediaPlayer != null) {
+ AndroidMediaPlayer.this.lockMuteOnPreparedCount.lock();
+ try {
+ if (AndroidMediaPlayer.this.muteOnPreparedCount > 0) {
+ AndroidMediaPlayer.this.muteOnPreparedCount--;
+ }
+ else {
+ AndroidMediaPlayer.this.muteOnPreparedCount = 0;
+ if (AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) {
+ Log.d(AMP_TAG, "Invoking AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared");
+ AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared(AndroidMediaPlayer.this.owningMediaPlayer);
+ }
+ }
+ }
+ finally {
+ AndroidMediaPlayer.this.lockMuteOnPreparedCount.unlock();
+ }
+ if (owningMediaPlayer.mpi != AndroidMediaPlayer.this) {
+ Log.d(AMP_TAG, "owningMediaPlayer has changed implementation");
+ }
+ }
+ }
+ };
+
+ private android.media.MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() {
+ public void onSeekComplete(android.media.MediaPlayer mp) {
+ if (owningMediaPlayer != null) {
+ owningMediaPlayer.lock.lock();
+ try {
+ lockMuteOnSeekCount.lock();
+ try {
+ if (AndroidMediaPlayer.this.muteOnSeekCount > 0) {
+ AndroidMediaPlayer.this.muteOnSeekCount--;
+ }
+ else {
+ AndroidMediaPlayer.this.muteOnSeekCount = 0;
+ if (AndroidMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) {
+ owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer);
+ }
+ }
+ }
+ finally {
+ lockMuteOnSeekCount.unlock();
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+ }
+ };
+
+ public AndroidMediaPlayer(com.aocate.media.MediaPlayer owningMediaPlayer, Context context) {
+ super(owningMediaPlayer, context);
+
+ mp = new MediaPlayer();
+
+// final ReentrantLock lock = new ReentrantLock();
+// Handler handler = new Handler(Looper.getMainLooper()) {
+// @Override
+// public void handleMessage(Message msg) {
+// Log.d(AMP_TAG, "Instantiating new AndroidMediaPlayer from Handler");
+// lock.lock();
+// if (mp == null) {
+// mp = new MediaPlayer();
+// }
+// lock.unlock();
+// }
+// };
+//
+// long endTime = System.currentTimeMillis() + TIMEOUT_DURATION_MS;
+//
+// while (true) {
+// // Retry messages until mp isn't null or it's time to give up
+// handler.sendMessage(handler.obtainMessage());
+// if ((mp != null)
+// || (endTime < System.currentTimeMillis())) {
+// break;
+// }
+// try {
+// Thread.sleep(50);
+// } catch (InterruptedException e) {
+// // TODO Auto-generated catch block
+// e.printStackTrace();
+// }
+// }
+
+ if (mp == null) {
+ throw new IllegalStateException("Did not instantiate android.media.MediaPlayer successfully");
+ }
+
+ mp.setOnBufferingUpdateListener(this.onBufferingUpdateListener);
+ mp.setOnCompletionListener(this.onCompletionListener);
+ mp.setOnErrorListener(this.onErrorListener);
+ mp.setOnInfoListener(this.onInfoListener);
+ Log.d(AMP_TAG, " ++++++++++++++++++++++++++++++++ Setting prepared listener to this.onPreparedListener");
+ mp.setOnPreparedListener(this.onPreparedListener);
+ mp.setOnSeekCompleteListener(this.onSeekCompleteListener);
+ }
+
+ @Override
+ public boolean canSetPitch() {
+ return false;
+ }
+
+ @Override
+ public boolean canSetSpeed() {
+ return false;
+ }
+
+ @Override
+ public float getCurrentPitchStepsAdjustment() {
+ return 0;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ owningMediaPlayer.lock.lock();
+ try {
+ return mp.getCurrentPosition();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public float getCurrentSpeedMultiplier() {
+ return 1f;
+ }
+
+ @Override
+ public int getDuration() {
+ owningMediaPlayer.lock.lock();
+ try {
+ return mp.getDuration();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public float getMaxSpeedMultiplier() {
+ return 1f;
+ }
+
+ @Override
+ public float getMinSpeedMultiplier() {
+ return 1f;
+ }
+
+ @Override
+ public boolean isLooping() {
+ owningMediaPlayer.lock.lock();
+ try {
+ return mp.isLooping();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isPlaying() {
+ owningMediaPlayer.lock.lock();
+ try {
+ return mp.isPlaying();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void pause() {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.pause();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void prepare() throws IllegalStateException, IOException {
+ owningMediaPlayer.lock.lock();
+ Log.d(AMP_TAG, "prepare()");
+ try {
+ mp.prepare();
+ Log.d(AMP_TAG, "Finish prepare()");
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void prepareAsync() {
+ mp.prepareAsync();
+ }
+
+ @Override
+ public void release() {
+ owningMediaPlayer.lock.lock();
+ try {
+ if (mp != null) {
+ Log.d(AMP_TAG, "mp.release()");
+ mp.release();
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void reset() {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.reset();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void seekTo(int msec) throws IllegalStateException {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.setOnSeekCompleteListener(this.onSeekCompleteListener);
+ mp.seekTo(msec);
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void setAudioStreamType(int streamtype) {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.setAudioStreamType(streamtype);
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void setDataSource(Context context, Uri uri)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ owningMediaPlayer.lock.lock();
+ try {
+ Log.d(AMP_TAG, "setDataSource(context, " + uri.toString() + ")");
+ mp.setDataSource(context, uri);
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void setDataSource(String path) throws IllegalArgumentException,
+ IllegalStateException, IOException {
+ owningMediaPlayer.lock.lock();
+ try {
+ Log.d(AMP_TAG, "setDataSource(" + path + ")");
+ mp.setDataSource(path);
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) {
+ // Can't!
+ }
+
+ @Override
+ public void setLooping(boolean loop) {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.setLooping(loop);
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void setPitchStepsAdjustment(float pitchSteps) {
+ // Can't!
+ }
+
+ @Override
+ public void setPlaybackPitch(float f) {
+ // Can't!
+ }
+
+ @Override
+ public void setPlaybackSpeed(float f) {
+ // Can't!
+ Log.d(AMP_TAG, "setPlaybackSpeed(" + f + ")");
+ }
+
+ @Override
+ public void setSpeedAdjustmentAlgorithm(int algorithm) {
+ // Can't!
+ Log.d(AMP_TAG, "setSpeedAdjustmentAlgorithm(" + algorithm + ")");
+ }
+
+ @Override
+ public void setVolume(float leftVolume, float rightVolume) {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.setVolume(leftVolume, rightVolume);
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void setWakeMode(Context context, int mode) {
+ owningMediaPlayer.lock.lock();
+ try {
+ if (mode != 0) {
+ mp.setWakeMode(context, mode);
+ }
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void start() {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.start();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+
+ @Override
+ public void stop() {
+ owningMediaPlayer.lock.lock();
+ try {
+ mp.stop();
+ }
+ finally {
+ owningMediaPlayer.lock.unlock();
+ }
+ }
+}
diff --git a/app/src/main/java/com/aocate/media/MediaPlayer.java b/app/src/main/java/com/aocate/media/MediaPlayer.java
new file mode 100644
index 000000000..04ecd58a9
--- /dev/null
+++ b/app/src/main/java/com/aocate/media/MediaPlayer.java
@@ -0,0 +1,1296 @@
+// 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 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/app/src/main/java/com/aocate/media/MediaPlayerImpl.java b/app/src/main/java/com/aocate/media/MediaPlayerImpl.java
new file mode 100644
index 000000000..856ab47ce
--- /dev/null
+++ b/app/src/main/java/com/aocate/media/MediaPlayerImpl.java
@@ -0,0 +1,118 @@
+// 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.concurrent.locks.ReentrantLock;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+public abstract class MediaPlayerImpl {
+ private static final String MPI_TAG = "AocateMediaPlayerImpl";
+ protected final MediaPlayer owningMediaPlayer;
+ protected final Context mContext;
+ protected int muteOnPreparedCount = 0;
+ protected int muteOnSeekCount = 0;
+
+ public MediaPlayerImpl(MediaPlayer owningMediaPlayer, Context context) {
+ this.owningMediaPlayer = owningMediaPlayer;
+
+ this.mContext = context;
+ }
+
+ public abstract boolean canSetPitch();
+
+ public abstract boolean canSetSpeed();
+
+ public abstract float getCurrentPitchStepsAdjustment();
+
+ public abstract int getCurrentPosition();
+
+ public abstract float getCurrentSpeedMultiplier();
+
+ public abstract int getDuration();
+
+ public abstract float getMaxSpeedMultiplier();
+
+ public abstract float getMinSpeedMultiplier();
+
+ public abstract boolean isLooping();
+
+ public abstract boolean isPlaying();
+
+ public abstract void pause();
+
+ public abstract void prepare() throws IllegalStateException, IOException;
+
+ public abstract void prepareAsync();
+
+ public abstract void release();
+
+ public abstract void reset();
+
+ public abstract void seekTo(int msec) throws IllegalStateException;
+
+ public abstract void setAudioStreamType(int streamtype);
+
+ public abstract void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException;
+
+ public abstract void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException;
+
+ public abstract void setEnableSpeedAdjustment(boolean enableSpeedAdjustment);
+
+ public abstract void setLooping(boolean loop);
+
+ public abstract void setPitchStepsAdjustment(float pitchSteps);
+
+ public abstract void setPlaybackPitch(float f);
+
+ public abstract void setPlaybackSpeed(float f);
+
+ public abstract void setSpeedAdjustmentAlgorithm(int algorithm);
+
+ public abstract void setVolume(float leftVolume, float rightVolume);
+
+ public abstract void setWakeMode(Context context, int mode);
+
+ public abstract void start();
+
+ public abstract void stop();
+
+ protected ReentrantLock lockMuteOnPreparedCount = new ReentrantLock();
+ public void muteNextOnPrepare() {
+ lockMuteOnPreparedCount.lock();
+ Log.d(MPI_TAG, "muteNextOnPrepare()");
+ try {
+ this.muteOnPreparedCount++;
+ }
+ finally {
+ lockMuteOnPreparedCount.unlock();
+ }
+ }
+
+ protected ReentrantLock lockMuteOnSeekCount = new ReentrantLock();
+ public void muteNextSeek() {
+ lockMuteOnSeekCount.lock();
+ Log.d(MPI_TAG, "muteNextOnSeek()");
+ try {
+ this.muteOnSeekCount++;
+ }
+ finally {
+ lockMuteOnSeekCount.unlock();
+ }
+ }
+}
diff --git a/app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java b/app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java
new file mode 100644
index 000000000..ef4572d33
--- /dev/null
+++ b/app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java
@@ -0,0 +1,1170 @@
+// 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 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;
+
+/**
+ * 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 + ")");
+ 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);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * 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.acquire();
+ }
+ }
+
+ 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");
+ 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();
+ 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);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java b/app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java
new file mode 100644
index 000000000..d337a0452
--- /dev/null
+++ b/app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java
@@ -0,0 +1,31 @@
+// 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;
+
+public class SpeedAdjustmentAlgorithm {
+ /**
+ * Use this to use the user-specified algorithm
+ */
+ public static int DEFAULT = 0;
+
+ /**
+ * Better for voice audio
+ */
+ public static int SONIC = 1;
+ /**
+ * Better for music audio
+ */
+ public static int WSOLA = 2;
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/AppConfig.java b/app/src/main/java/de/danoeh/antennapod/AppConfig.java
new file mode 100644
index 000000000..7a75e3a18
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/AppConfig.java
@@ -0,0 +1,7 @@
+package de.danoeh.antennapod;
+
+public final class AppConfig {
+ /** Should be used when setting User-Agent header for HTTP-requests. */
+ public final static String USER_AGENT = "AntennaPod/0.9.9.3";
+
+}
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..74628f3d6
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java
@@ -0,0 +1,47 @@
+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/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java
new file mode 100644
index 000000000..cf7de1709
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.activity;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import de.danoeh.antennapod.R;
+
+/** Displays the 'about' screen */
+public class AboutActivity extends ActionBarActivity {
+
+ private WebView webview;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().hide();
+ setContentView(R.layout.about);
+ webview = (WebView) findViewById(R.id.webvAbout);
+ webview.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ view.loadUrl(url);
+ return false;
+ }
+
+ });
+ webview.loadUrl("file:///android_asset/about.html");
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java
new file mode 100644
index 000000000..18d27ddda
--- /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.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 adapter = (ArrayAdapter) 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 loadTask;
+
+ private void loadData() {
+ loadTask = new AsyncTask() {
+ @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..a03fa7949
--- /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.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 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 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 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) {
+ 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 alternateUrlsList = new ArrayList();
+ final List alternateUrlsTitleList = new ArrayList();
+
+ alternateUrlsList.add(feed.getDownload_url());
+ alternateUrlsTitleList.add(feed.getTitle());
+
+
+ alternateUrlsList.addAll(alternateFeedUrls.keySet());
+ for (String url : alternateFeedUrls.keySet()) {
+ alternateUrlsTitleList.add(alternateFeedUrls.get(url));
+ }
+ ArrayAdapter adapter = new ArrayAdapter(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>() {
+ @Override
+ protected List doInBackground(Void... params) {
+ return DBReader.getFeedList(DefaultOnlineFeedViewActivity.this);
+ }
+
+ @Override
+ protected void onPostExecute(List 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..06a11c775
--- /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.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 listDirectoriesAdapter;
+ private ArrayList 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();
+ listDirectoriesAdapter = new ArrayAdapter(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..c5f25d813
--- /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.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/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..5cf187eb6
--- /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.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 loadTask = new AsyncTask() {
+
+ @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..8dde14d3b
--- /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.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/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..b7014dab2
--- /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.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 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 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() {
+ @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..2e5372b60
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -0,0 +1,525 @@
+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/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..2c6d75cd8
--- /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.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.
+ *
+ * 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 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 alternateFeedUrls) {
+
+ }
+
+ /**
+ * Called when feed parsed successfully.
+ * This method is executed on the GUI thread.
+ */
+ protected void showFeedInformation(Feed feed, Map 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 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 titles = new ArrayList();
+ final List urls = new ArrayList();
+
+ urls.addAll(urlsMap.keySet());
+ for (String url : urls) {
+ titles.add(urlsMap.get(url));
+ }
+
+ final ArrayAdapter adapter = new ArrayAdapter(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..e09941abf
--- /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.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 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(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 getTitleList() {
+ List result = new ArrayList();
+ 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..d3fd3949c
--- /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.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 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..16e663fac
--- /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.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/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..94f100321
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
@@ -0,0 +1,172 @@
+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/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..ec53ed7b6
--- /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.opml.OpmlElement;
+
+import java.util.ArrayList;
+
+/**
+ * Hold infos gathered by Ompl-Import
+ *
+ * Created with IntelliJ IDEA.
+ * User: ligi
+ * Date: 1/23/13
+ * Time: 2:15 PM
+ */
+public class OpmlImportHolder {
+
+ private static ArrayList readElements;
+
+ public static ArrayList getReadElements() {
+ return readElements;
+ }
+
+ public static void setReadElements(ArrayList _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..cd6731c02
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
@@ -0,0 +1,513 @@
+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.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 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);
+ 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 networks = wifiservice.getConfiguredNetworks();
+
+ if (networks != null) {
+ selectedNetworks = new CheckBoxPreference[networks.size()];
+ List 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 prefValuesList = new ArrayList(
+ 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..d8a137eb9
--- /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.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/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..81661a288
--- /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.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 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 {
+ @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..6a60f65fe
--- /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.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() {
+
+ 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> devices = new AtomicReference>();
+ new AsyncTask>() {
+
+ private volatile Exception exception;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ chooseDevice.setEnabled(false);
+ spinnerDevices.setEnabled(false);
+ createNewDevice.setEnabled(false);
+ }
+
+ @Override
+ protected void onPostExecute(List gpodnetDevices) {
+ super.onPostExecute(gpodnetDevices);
+ if (gpodnetDevices != null) {
+ List deviceNames = new ArrayList();
+ for (GpodnetDevice device : gpodnetDevices) {
+ deviceNames.add(device.getCaption());
+ }
+ spinnerDevices.setAdapter(new ArrayAdapter(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 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() {
+
+ 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 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..30ad2d03f
--- /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.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..1de071a73
--- /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.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/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..f393fb7d7
--- /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.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/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..c12de6ebd
--- /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.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 {
+
+ private static final String TAG = "ChapterListAdapter";
+
+ private List chapters;
+ private Playable media;
+
+ private int defaultTextColor;
+
+ public ChapterListAdapter(Context context, int textViewResourceId,
+ List 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..0c4cbe685
--- /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.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/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..2cc216227
--- /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.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/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..ef5af67de
--- /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.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/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..658af9e4e
--- /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.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/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..3f666eb8b
--- /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.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/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..357b5f8b4
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
@@ -0,0 +1,220 @@
+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/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..c2c2285ac
--- /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.feed.FeedItem;
+
+import java.util.List;
+
+/**
+ * List adapter for showing a list of FeedItems with their title and description.
+ */
+public class FeedItemlistDescriptionAdapter extends ArrayAdapter {
+
+ public FeedItemlistDescriptionAdapter(Context context, int resource, List 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..ef8e8ce07
--- /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.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/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..8abe49133
--- /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.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/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..ebe519592
--- /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.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/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..2314c2269
--- /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.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/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..f2e78a57e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java
@@ -0,0 +1,64 @@
+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 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 {
+
+ public PodcastListAdapter(Context context, int resource, List 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());
+
+ 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/DownloadObserver.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DownloadObserver.java
new file mode 100644
index 000000000..21ae5291e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/DownloadObserver.java
@@ -0,0 +1,177 @@
+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 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 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 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/app/src/main/java/de/danoeh/antennapod/asynctask/FeedRemover.java b/app/src/main/java/de/danoeh/antennapod/asynctask/FeedRemover.java
new file mode 100644
index 000000000..0549a4255
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/FeedRemover.java
@@ -0,0 +1,74 @@
+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 {
+ 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/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrClickWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
new file mode 100644
index 000000000..9210ac1d1
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrClickWorker.java
@@ -0,0 +1,238 @@
+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.
+ *
+ * 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.
+ *
+ * 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 {
+ 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.
+ *
+ * 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.
+ *
+ * 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 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 dbFutures = new LinkedList();
+ 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/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java b/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
new file mode 100644
index 000000000..04d349671
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java
@@ -0,0 +1,47 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.content.Context;
+import android.util.Log;
+import de.danoeh.antennapod.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 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/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java b/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java
new file mode 100644
index 000000000..0dcf832f7
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java
@@ -0,0 +1,95 @@
+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 {
+ 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/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..4abb1a67d
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java
@@ -0,0 +1,114 @@
+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 {
+ 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/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..038b8dcc5
--- /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.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 {
+ 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..13534fa64
--- /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.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> {
+ 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 doInBackground(Void... params) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Starting background work");
+
+ if (mReader==null) {
+ return null;
+ }
+
+ OpmlReader opmlReader = new OpmlReader();
+ try {
+ ArrayList 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 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/asynctask/PicassoImageResource.java b/app/src/main/java/de/danoeh/antennapod/asynctask/PicassoImageResource.java
new file mode 100644
index 000000000..26f9d9278
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/PicassoImageResource.java
@@ -0,0 +1,37 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.net.Uri;
+
+/**
+ * Classes that implement this interface provide access to an image resource that can
+ * be loaded by the Picasso library.
+ */
+public interface PicassoImageResource {
+
+ /**
+ * This scheme should be used by PicassoImageResources to
+ * indicate that the image Uri points to a file that is not an image
+ * (e.g. a media file). This workaround is needed so that the Picasso library
+ * loads these Uri with a Downloader instead of trying to load it directly.
+ *
+ * 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.
+ *
+ * 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).
+ *
+ * The Uri can also have an optional fallback-URL if loading the default URL
+ * failed (see PARAM_FALLBACK).
+ */
+ public Uri getImageUri();
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/PicassoProvider.java b/app/src/main/java/de/danoeh/antennapod/asynctask/PicassoProvider.java
new file mode 100644
index 000000000..849725630
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/PicassoProvider.java
@@ -0,0 +1,152 @@
+package de.danoeh.antennapod.asynctask;
+
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.squareup.picasso.Cache;
+import com.squareup.picasso.Downloader;
+import com.squareup.picasso.LruCache;
+import com.squareup.picasso.OkHttpDownloader;
+import com.squareup.picasso.Picasso;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Provides access to Picasso instances.
+ */
+public class PicassoProvider {
+ private static final String TAG = "PicassoProvider";
+
+ private static final boolean DEBUG = false;
+
+ private static ExecutorService executorService;
+ private static Cache memoryCache;
+
+ private static Picasso defaultPicassoInstance;
+ private static Picasso mediaMetadataPicassoInstance;
+
+ private static synchronized ExecutorService getExecutorService() {
+ if (executorService == null) {
+ executorService = Executors.newFixedThreadPool(3);
+ }
+ return executorService;
+ }
+
+ private static synchronized Cache getMemoryCache(Context context) {
+ if (memoryCache == null) {
+ memoryCache = new LruCache(context);
+ }
+ return memoryCache;
+ }
+
+ /**
+ * Returns a Picasso instance that uses an OkHttpDownloader. This instance can only load images
+ * from image files.
+ *
+ * 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/app/src/main/java/de/danoeh/antennapod/backup/OpmlBackupAgent.java b/app/src/main/java/de/danoeh/antennapod/backup/OpmlBackupAgent.java
new file mode 100644
index 000000000..56d1ca092
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/backup/OpmlBackupAgent.java
@@ -0,0 +1,212 @@
+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 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/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java
new file mode 100644
index 000000000..bdb2d68ba
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import de.danoeh.antennapod.R;
+
+/**
+ * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
+ */
+public abstract class AuthenticationDialog extends Dialog {
+
+ private final int titleRes;
+ private final boolean enableUsernameField;
+ private final boolean showSaveCredentialsCheckbox;
+ private final String usernameInitialValue;
+ private final String passwordInitialValue;
+
+ public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, boolean showSaveCredentialsCheckbox, String usernameInitialValue, String passwordInitialValue) {
+ super(context);
+ this.titleRes = titleRes;
+ this.enableUsernameField = enableUsernameField;
+ this.showSaveCredentialsCheckbox = showSaveCredentialsCheckbox;
+ this.usernameInitialValue = usernameInitialValue;
+ this.passwordInitialValue = passwordInitialValue;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.authentication_dialog);
+ final EditText etxtUsername = (EditText) findViewById(R.id.etxtUsername);
+ final EditText etxtPassword = (EditText) findViewById(R.id.etxtPassword);
+ final CheckBox saveUsernamePassword = (CheckBox) findViewById(R.id.chkSaveUsernamePassword);
+ final Button butConfirm = (Button) findViewById(R.id.butConfirm);
+ final Button butCancel = (Button) findViewById(R.id.butCancel);
+
+ if (titleRes != 0) {
+ setTitle(titleRes);
+ } else {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+ etxtUsername.setEnabled(enableUsernameField);
+ if (showSaveCredentialsCheckbox) {
+ saveUsernamePassword.setVisibility(View.VISIBLE);
+ } else {
+ saveUsernamePassword.setVisibility(View.GONE);
+ }
+ if (usernameInitialValue != null) {
+ etxtUsername.setText(usernameInitialValue);
+ }
+ if (passwordInitialValue != null) {
+ etxtPassword.setText(passwordInitialValue);
+ }
+ setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onCancelled();
+ }
+ });
+ butCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancel();
+ }
+ });
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onConfirmed(etxtUsername.getText().toString(),
+ etxtPassword.getText().toString(),
+ showSaveCredentialsCheckbox && saveUsernamePassword.isChecked());
+ dismiss();
+ }
+ });
+ }
+
+ protected void onCancelled() {
+
+ }
+
+ protected abstract void onConfirmed(String username, String password, boolean saveUsernamePassword);
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java
new file mode 100644
index 000000000..d1ed795dc
--- /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.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/ConfirmationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ConfirmationDialog.java
new file mode 100644
index 000000000..df71fff77
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/ConfirmationDialog.java
@@ -0,0 +1,64 @@
+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/app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java b/app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java
new file mode 100644
index 000000000..e363a6911
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java
@@ -0,0 +1,30 @@
+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/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..7384463de
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java
@@ -0,0 +1,428 @@
+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.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());
+ 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 loadTask = new AsyncTask() {
+ String data;
+
+
+ private String applyWebviewStyle(String textColor, String data) {
+ final String WEBVIEW_STYLE = "%s";
+ 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 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... collections) {
+ setQueue(queue);
+
+ boolean setItemFromCollectionResult = false;
+ if (collections != null) {
+ for (List 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 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..a9c596d2e
--- /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.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/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..bbd514640
--- /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.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 spinnerAdapter = new ArrayAdapter(
+ 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..b009e76a7
--- /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.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 selectedSpeedList = Arrays.asList(UserPreferences
+ .getPlaybackSpeedArray());
+ for (int i = 0; i < speedValues.length; i++) {
+ speedChecked[i] = selectedSpeedList.contains(speedValues[i]);
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.set_playback_speed_label);
+ builder.setMultiChoiceItems(R.array.playback_speed_values,
+ speedChecked, new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which,
+ boolean isChecked) {
+ speedChecked[which] = isChecked;
+ }
+
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int choiceCount = 0;
+ for (int i = 0; i < speedChecked.length; i++) {
+ if (speedChecked[i]) {
+ choiceCount++;
+ }
+ }
+ String[] newSpeedValues = new String[choiceCount];
+ int newSpeedIndex = 0;
+ for (int i = 0; i < speedChecked.length; i++) {
+ if (speedChecked[i]) {
+ newSpeedValues[newSpeedIndex++] = speedValues[i];
+ }
+ }
+
+ UserPreferences.setPlaybackSpeedArray(newSpeedValues);
+
+ }
+ });
+ builder.create().show();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/feed/Chapter.java b/app/src/main/java/de/danoeh/antennapod/feed/Chapter.java
new file mode 100644
index 000000000..d6151ee9f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/Chapter.java
@@ -0,0 +1,55 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java b/app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java
new file mode 100644
index 000000000..5fb72048e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java
@@ -0,0 +1,140 @@
+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 events;
+
+ private static EventDistributor instance;
+
+ private EventDistributor() {
+ this.handler = new Handler();
+ events = new ConcurrentLinkedQueue();
+ }
+
+ 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/app/src/main/java/de/danoeh/antennapod/feed/Feed.java b/app/src/main/java/de/danoeh/antennapod/feed/Feed.java
new file mode 100644
index 000000000..b5415c69c
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/Feed.java
@@ -0,0 +1,445 @@
+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 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();
+ }
+
+ /**
+ * 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();
+ 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 getItems() {
+ return items;
+ }
+
+ public void setItems(List 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/app/src/main/java/de/danoeh/antennapod/feed/FeedComponent.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedComponent.java
new file mode 100644
index 000000000..48b243770
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/FeedComponent.java
@@ -0,0 +1,66 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java
new file mode 100644
index 000000000..a05533ebc
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java
@@ -0,0 +1,105 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java
new file mode 100644
index 000000000..c588f5e71
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java
@@ -0,0 +1,77 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java
new file mode 100644
index 000000000..78091ea33
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java
@@ -0,0 +1,333 @@
+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 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 getChapters() {
+ return chapters;
+ }
+
+ public void setChapters(List 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 loadShownotes() {
+ return new Callable() {
+ @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/app/src/main/java/de/danoeh/antennapod/feed/FeedMedia.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedMedia.java
new file mode 100644
index 000000000..9298ebe8a
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/FeedMedia.java
@@ -0,0 +1,411 @@
+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 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 chapters) {
+ getItem().setChapters(chapters);
+ }
+
+ @Override
+ public Callable loadShownotes() {
+ return new Callable() {
+ @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 CREATOR = new Parcelable.Creator() {
+ 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/app/src/main/java/de/danoeh/antennapod/feed/FeedPreferences.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedPreferences.java
new file mode 100644
index 000000000..29bc5ef0c
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/FeedPreferences.java
@@ -0,0 +1,89 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java b/app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java
new file mode 100644
index 000000000..6dde7854e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java
@@ -0,0 +1,36 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/MediaType.java b/app/src/main/java/de/danoeh/antennapod/feed/MediaType.java
new file mode 100644
index 000000000..324d0a221
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/MediaType.java
@@ -0,0 +1,5 @@
+package de.danoeh.antennapod.feed;
+
+public enum MediaType {
+ AUDIO, VIDEO, UNKNOWN
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java b/app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java
new file mode 100644
index 000000000..1cba389ec
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java
@@ -0,0 +1,34 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java b/app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java
new file mode 100644
index 000000000..3dab1b74d
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java
@@ -0,0 +1,25 @@
+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/app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java b/app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java
new file mode 100644
index 000000000..59844ae1f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java
@@ -0,0 +1,109 @@
+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/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
new file mode 100644
index 000000000..f5ae5a777
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
@@ -0,0 +1,76 @@
+package de.danoeh.antennapod.fragment;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
+import de.danoeh.antennapod.activity.OpmlImportFromPathActivity;
+import de.danoeh.antennapod.fragment.gpodnet.GpodnetMainFragment;
+
+/**
+ * Provides actions for adding new podcast subscriptions
+ */
+public class AddFeedFragment extends Fragment {
+ private static final String TAG = "AddFeedFragment";
+
+ /**
+ * Preset value for url text field.
+ */
+ public static final String ARG_FEED_URL = "feedurl";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View root = inflater.inflate(R.layout.addfeed, container, false);
+
+ final EditText etxtFeedurl = (EditText) root.findViewById(R.id.etxtFeedurl);
+
+ Bundle args = getArguments();
+ if (args != null && args.getString(ARG_FEED_URL) != null) {
+ etxtFeedurl.setText(args.getString(ARG_FEED_URL));
+ }
+
+ Button butBrowserGpoddernet = (Button) root.findViewById(R.id.butBrowseGpoddernet);
+ Button butOpmlImport = (Button) root.findViewById(R.id.butOpmlImport);
+ Button butConfirm = (Button) root.findViewById(R.id.butConfirm);
+
+ final MainActivity activity = (MainActivity) getActivity();
+ activity.getMainActivtyActionBar().setTitle(R.string.add_feed_label);
+
+ butBrowserGpoddernet.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ activity.loadChildFragment(new GpodnetMainFragment());
+ }
+ });
+
+ butOpmlImport.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(getActivity(),
+ OpmlImportFromPathActivity.class));
+ }
+ });
+
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class);
+ intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, etxtFeedurl.getText().toString());
+ intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, getString(R.string.add_feed_label));
+ startActivity(intent);
+ }
+ });
+
+ return root;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
new file mode 100644
index 000000000..082fe93fc
--- /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.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 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 {
+
+ @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) 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..ffce518bf
--- /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.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/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..d81ba4b86
--- /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.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 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> {
+
+ @Override
+ protected void onPostExecute(List downloadStatuses) {
+ super.onPostExecute(downloadStatuses);
+ if (downloadStatuses != null) {
+ downloadLog = downloadStatuses;
+ itemsLoaded = true;
+ if (viewsCreated) {
+ onFragmentLoaded();
+ }
+ }
+ }
+
+ @Override
+ protected List doInBackground(Void... params) {
+ Context context = getActivity();
+ if (context != null) {
+ return DBReader.getDownloadLog(context);
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
new file mode 100644
index 000000000..5a71cb36b
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
@@ -0,0 +1,145 @@
+package de.danoeh.antennapod.fragment;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+
+/**
+ * Shows the CompletedDownloadsFragment and the RunningDownloadsFragment
+ */
+public class DownloadsFragment extends Fragment {
+
+ public static final String ARG_SELECTED_TAB = "selected_tab";
+
+ public static final int POS_RUNNING = 0;
+ public static final int POS_COMPLETED = 1;
+ public static final int POS_LOG = 2;
+
+ private ViewPager pager;
+ private MainActivity activity;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View root = inflater.inflate(R.layout.pager_fragment, container, false);
+ pager = (ViewPager) root.findViewById(R.id.pager);
+ DownloadsPagerAdapter pagerAdapter = new DownloadsPagerAdapter(getChildFragmentManager(), getResources());
+ pager.setAdapter(pagerAdapter);
+ final ActionBar actionBar = activity.getMainActivtyActionBar();
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ ActionBar.TabListener tabListener = new ActionBar.TabListener() {
+ @Override
+ public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+ pager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+
+ @Override
+ public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+ };
+ actionBar.removeAllTabs();
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.downloads_running_label)
+ .setTabListener(tabListener));
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.downloads_completed_label)
+ .setTabListener(tabListener));
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.downloads_log_label)
+ .setTabListener(tabListener));
+
+ pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ actionBar.setSelectedNavigationItem(position);
+ }
+ });
+ return root;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (getArguments() != null) {
+ int tab = getArguments().getInt(ARG_SELECTED_TAB);
+ pager.setCurrentItem(tab, false);
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ this.activity = (MainActivity) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ activity.getMainActivtyActionBar().removeAllTabs();
+ activity.getMainActivtyActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+
+ public class DownloadsPagerAdapter extends FragmentPagerAdapter {
+
+
+
+
+ Resources resources;
+
+ public DownloadsPagerAdapter(FragmentManager fm, Resources resources) {
+ super(fm);
+ this.resources = resources;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case POS_RUNNING:
+ return new RunningDownloadsFragment();
+ case POS_COMPLETED:
+ return new CompletedDownloadsFragment();
+ case POS_LOG:
+ return new DownloadLogFragment();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return 3;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case POS_RUNNING:
+ return resources.getString(R.string.downloads_running_label);
+ case POS_COMPLETED:
+ return resources.getString(R.string.downloads_completed_label);
+ case POS_LOG:
+ return resources.getString(R.string.downloads_log_label);
+ default:
+ return super.getPageTitle(position);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
new file mode 100644
index 000000000..985673dd3
--- /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.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/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..04c7fbf8e
--- /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.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 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 itemLoadTask = new AsyncTask() {
+
+ @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 createLoader() {
+ return new AsyncTask() {
+ @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..909774467
--- /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.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 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 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 {
+ @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..fe995256b
--- /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.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 unreadItems;
+ private List recentItems;
+ private QueueAccess queueAccess;
+ private List downloaderList;
+
+ private boolean itemsLoaded = false;
+ private boolean viewsCreated = false;
+ private boolean showOnlyNewEpisodes = false;
+
+ private AtomicReference activity = new AtomicReference();
+
+ 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 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 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 {
+
+ @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) lists[0];
+ recentItems = (List) 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..470186180
--- /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.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 playbackHistory;
+ private QueueAccess queue;
+ private FeedItemlistAdapter adapter;
+
+ private boolean itemsLoaded = false;
+ private boolean viewsCreated = false;
+
+ private AtomicReference activity = new AtomicReference();
+
+ private DownloadObserver downloadObserver;
+ private List 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 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 {
+
+ @Override
+ protected Object[] doInBackground(Void... params) {
+ Context context = activity.get();
+ if (context != null) {
+ List 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) 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..2f322f75b
--- /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.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 queue;
+ private List downloaderList;
+
+ private boolean itemsLoaded = false;
+ private boolean viewsCreated = false;
+
+ private AtomicReference activity = new AtomicReference();
+
+ 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 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> {
+ @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 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 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..89c30e34b
--- /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.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 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 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..b3ade4d70
--- /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.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 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.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 {
+ @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) results[0];
+ queue = (QueueAccess) results[1];
+ if (viewCreated) {
+ onFragmentLoaded();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
new file mode 100644
index 000000000..ec8f69368
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
@@ -0,0 +1,131 @@
+package de.danoeh.antennapod.fragment.gpodnet;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+
+/**
+ * Main navigation hub for gpodder.net podcast directory
+ */
+public class GpodnetMainFragment extends Fragment {
+
+ private ViewPager pager;
+ private MainActivity activity;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View root = inflater.inflate(R.layout.pager_fragment, container, false);
+ pager = (ViewPager) root.findViewById(R.id.pager);
+ GpodnetPagerAdapter pagerAdapter = new GpodnetPagerAdapter(getChildFragmentManager(), getResources());
+ pager.setAdapter(pagerAdapter);
+ final ActionBar actionBar = activity.getMainActivtyActionBar();
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ ActionBar.TabListener tabListener = new ActionBar.TabListener() {
+ @Override
+ public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+ pager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+
+ @Override
+ public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
+
+ }
+ };
+ actionBar.removeAllTabs();
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.gpodnet_taglist_header)
+ .setTabListener(tabListener));
+ actionBar.addTab(actionBar.newTab()
+ .setText(R.string.gpodnet_toplist_header)
+ .setTabListener(tabListener));
+ actionBar.setTitle(R.string.gpodnet_main_label);
+
+ pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ actionBar.setSelectedNavigationItem(position);
+ }
+ });
+ return root;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ activity.getMainActivtyActionBar().removeAllTabs();
+ activity.getMainActivtyActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ this.activity = (MainActivity) activity;
+ }
+
+ public class GpodnetPagerAdapter extends FragmentPagerAdapter {
+
+
+ private static final int NUM_PAGES = 2;
+ private static final int POS_TAGS = 0;
+ private static final int POS_TOPLIST = 1;
+ private static final int POS_SUGGESTIONS = 2;
+
+ Resources resources;
+
+ public GpodnetPagerAdapter(FragmentManager fm, Resources resources) {
+ super(fm);
+ this.resources = resources;
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+ switch (i) {
+ case POS_TAGS:
+ return new TagListFragment();
+ case POS_TOPLIST:
+ return new PodcastTopListFragment();
+ case POS_SUGGESTIONS:
+ return new SuggestionListFragment();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case POS_TAGS:
+ return getString(R.string.gpodnet_taglist_header);
+ case POS_TOPLIST:
+ return getString(R.string.gpodnet_toplist_header);
+ case POS_SUGGESTIONS:
+ return getString(R.string.gpodnet_suggestions_header);
+ default:
+ return super.getPageTitle(position);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_PAGES;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
new file mode 100644
index 000000000..1b4616207
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
@@ -0,0 +1,169 @@
+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 loadPodcastData(GpodnetService service) throws GpodnetServiceException;
+
+ protected final void loadData() {
+ AsyncTask> loaderTask = new AsyncTask>() {
+ volatile Exception exception = null;
+
+ @Override
+ protected List 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 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..5717a74e7
--- /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.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 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..801024787
--- /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.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 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..45fe25580
--- /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.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 loadPodcastData(GpodnetService service) throws GpodnetServiceException {
+ if (GpodnetPreferences.loggedIn()) {
+ service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ return service.getSuggestions(SUGGESTIONS_COUNT);
+ } else {
+ return new ArrayList();
+ }
+ }
+}
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..204dda992
--- /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.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 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..a7e1033df
--- /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.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> loadTask;
+
+ private void cancelLoadTask() {
+ if (loadTask != null && !loadTask.isCancelled()) {
+ loadTask.cancel(true);
+ }
+ }
+
+ private void startLoadTask() {
+ cancelLoadTask();
+ loadTask = new AsyncTask>() {
+ private Exception exception;
+
+ @Override
+ protected List 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 gpodnetTags) {
+ super.onPostExecute(gpodnetTags);
+ final Context context = getActivity();
+ if (context != null) {
+ if (gpodnetTags != null) {
+ List tagNames = new ArrayList();
+ for (GpodnetTag tag : gpodnetTags) {
+ tagNames.add(tag.getName());
+ }
+ setListAdapter(new ArrayAdapter(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/gpoddernet/GpodnetService.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetService.java
new file mode 100644
index 000000000..038b2a367
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetService.java
@@ -0,0 +1,718 @@
+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 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 tagList = new ArrayList(
+ 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 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 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.
+ *
+ * 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 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 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.
+ *
+ * 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 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 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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 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.
+ *
+ * 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 added,
+ Collection 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.
+ *
+ * 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 readPodcastListFromJSONArray(JSONArray array)
+ throws JSONException {
+ Validate.notNull(array);
+
+ List result = new ArrayList(
+ 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 readDeviceListFromJSONArray(JSONArray array)
+ throws JSONException {
+ Validate.notNull(array);
+
+ List result = new ArrayList(
+ 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 added = new LinkedList();
+ JSONArray jsonAdded = object.getJSONArray("add");
+ for (int i = 0; i < jsonAdded.length(); i++) {
+ added.add(jsonAdded.getString(i));
+ }
+
+ List removed = new LinkedList();
+ 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/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java
new file mode 100644
index 000000000..3b0140826
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java
@@ -0,0 +1,21 @@
+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/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java
new file mode 100644
index 000000000..a32e9357b
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java
@@ -0,0 +1,12 @@
+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/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java
new file mode 100644
index 000000000..bdb394454
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java
@@ -0,0 +1,19 @@
+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/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java
new file mode 100644
index 000000000..86a2171fa
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java
@@ -0,0 +1,72 @@
+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/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java
new file mode 100644
index 000000000..b002035c9
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java
@@ -0,0 +1,65 @@
+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/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java
new file mode 100644
index 000000000..a4617118d
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java
@@ -0,0 +1,41 @@
+package de.danoeh.antennapod.gpoddernet.model;
+
+import org.apache.commons.lang3.Validate;
+
+import java.util.List;
+
+public class GpodnetSubscriptionChange {
+ private List added;
+ private List removed;
+ private long timestamp;
+
+ public GpodnetSubscriptionChange(List added, List 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 getAdded() {
+ return added;
+ }
+
+ public List getRemoved() {
+ return removed;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java
new file mode 100644
index 000000000..80b84095e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java
@@ -0,0 +1,46 @@
+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 {
+
+ @Override
+ public int compare(GpodnetTag o1, GpodnetTag o2) {
+ return o1.usage - o2.usage;
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java
new file mode 100644
index 000000000..fee8c7d28
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java
@@ -0,0 +1,56 @@
+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 updatedUrls;
+
+ public GpodnetUploadChangesResponse(long timestamp, Map 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 updatedUrls = new HashMap();
+ 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/app/src/main/java/de/danoeh/antennapod/opml/OpmlElement.java b/app/src/main/java/de/danoeh/antennapod/opml/OpmlElement.java
new file mode 100644
index 000000000..4cb563c04
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/opml/OpmlElement.java
@@ -0,0 +1,46 @@
+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/app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java b/app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java
new file mode 100644
index 000000000..19a980dee
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java
@@ -0,0 +1,87 @@
+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 elementList;
+
+ /**
+ * Reads an Opml document and returns a list of all OPML elements it can
+ * find
+ *
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ public ArrayList readDocument(Reader reader)
+ throws XmlPullParserException, IOException {
+ elementList = new ArrayList();
+ 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/app/src/main/java/de/danoeh/antennapod/opml/OpmlSymbols.java b/app/src/main/java/de/danoeh/antennapod/opml/OpmlSymbols.java
new file mode 100644
index 000000000..4b0b7316a
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/opml/OpmlSymbols.java
@@ -0,0 +1,21 @@
+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/app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java b/app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java
new file mode 100644
index 000000000..405a5e35a
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java
@@ -0,0 +1,65 @@
+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 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/app/src/main/java/de/danoeh/antennapod/preferences/GpodnetPreferences.java b/app/src/main/java/de/danoeh/antennapod/preferences/GpodnetPreferences.java
new file mode 100644
index 000000000..bdfe297a6
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/preferences/GpodnetPreferences.java
@@ -0,0 +1,246 @@
+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 addedFeeds;
+ private static Set 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 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 getAddedFeedsCopy() {
+ ensurePreferencesLoaded();
+ Set copy = new HashSet();
+ feedListLock.lock();
+ copy.addAll(addedFeeds);
+ feedListLock.unlock();
+ return copy;
+ }
+
+ public static void removeAddedFeeds(Collection removed) {
+ ensurePreferencesLoaded();
+ feedListLock.lock();
+ addedFeeds.removeAll(removed);
+ writePreference(PREF_SYNC_ADDED, addedFeeds);
+ feedListLock.unlock();
+ }
+
+ public static Set getRemovedFeedsCopy() {
+ ensurePreferencesLoaded();
+ Set copy = new HashSet();
+ feedListLock.lock();
+ copy.addAll(removedFeeds);
+ feedListLock.unlock();
+ return copy;
+ }
+
+ public static void removeRemovedFeeds(Collection 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 readListFromString(String s) {
+ Set result = new HashSet();
+ for (String item : s.split(" ")) {
+ result.add(item);
+ }
+ return result;
+ }
+
+ private static String writeListToString(Collection 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/app/src/main/java/de/danoeh/antennapod/preferences/PlaybackPreferences.java b/app/src/main/java/de/danoeh/antennapod/preferences/PlaybackPreferences.java
new file mode 100644
index 000000000..1d1ab052f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/preferences/PlaybackPreferences.java
@@ -0,0 +1,146 @@
+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/app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java b/app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java
new file mode 100644
index 000000000..2020ddfae
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java
@@ -0,0 +1,577 @@
+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.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";
+
+ // 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 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"));
+ }
+
+ 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 speedList = new LinkedList();
+ 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;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ 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/app/src/main/java/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java
new file mode 100644
index 000000000..a0539e276
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java
@@ -0,0 +1,33 @@
+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/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..4dcf0b6aa
--- /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.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/app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java
new file mode 100644
index 000000000..3c283a30b
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.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.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/app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
new file mode 100644
index 000000000..1edebd275
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
@@ -0,0 +1,32 @@
+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/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..9f8892181
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java
@@ -0,0 +1,50 @@
+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/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..b0430d170
--- /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.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/app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java b/app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java
new file mode 100644
index 000000000..c8c9fc31e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java
@@ -0,0 +1,245 @@
+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 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());
+ if (BuildConfig.DEBUG) Log.d(TAG, "Uploading changes response: " + uploadChangesResponse);
+ GpodnetPreferences.removeAddedFeeds(localSubscriptions);
+ GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy());
+ GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
+ } else {
+ Set added = GpodnetPreferences.getAddedFeedsCopy();
+ Set 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 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/app/src/main/java/de/danoeh/antennapod/service/download/APRedirectHandler.java b/app/src/main/java/de/danoeh/antennapod/service/download/APRedirectHandler.java
new file mode 100644
index 000000000..ddf8d605d
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/service/download/APRedirectHandler.java
@@ -0,0 +1,54 @@
+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 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 CREATOR = new Parcelable.Creator() {
+ 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/app/src/main/java/de/danoeh/antennapod/service/download/DownloadService.java b/app/src/main/java/de/danoeh/antennapod/service/download/DownloadService.java
new file mode 100644
index 000000000..63be91b57
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/service/download/DownloadService.java
@@ -0,0 +1,1230 @@
+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 newMediaFiles;
+
+ /**
+ * Contains all completed downloads that have not been included in the report yet.
+ */
+ private List reportQueue;
+
+ private ExecutorService syncExecutor;
+ private CompletionService 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 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());
+ reportQueue = Collections.synchronizedList(new ArrayList());
+ downloads = new ArrayList();
+ 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(
+ 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 completedRequests = new LinkedBlockingDeque();
+ private CompletionService parserService = new ExecutorCompletionService(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 collectCompletedRequests() {
+ List results = new LinkedList