From 21b5b835e3a9c83410120d38a63e51be2981a38b Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Fri, 19 Sep 2014 22:51:35 +0200 Subject: Moved non-gui classes to 'core' package --- .../antennapod/gpodnet/GPodnetServiceTest.java | 8 +- .../test/antennapod/handler/FeedHandlerTest.java | 6 +- .../service/download/HttpDownloaderTest.java | 12 +- .../playback/PlaybackServiceMediaPlayerTest.java | 14 +- .../playback/PlaybackServiceTaskManagerTest.java | 12 +- .../de/test/antennapod/storage/DBReaderTest.java | 15 +- .../de/test/antennapod/storage/DBTasksTest.java | 16 +- .../de/test/antennapod/storage/DBTestUtils.java | 12 +- .../de/test/antennapod/storage/DBWriterTest.java | 14 +- .../de/test/antennapod/ui/MainActivityTest.java | 4 +- .../java/de/test/antennapod/ui/PlaybackTest.java | 12 +- .../java/de/test/antennapod/ui/UITestUtils.java | 4 +- .../de/test/antennapod/ui/UITestUtilsTest.java | 4 +- .../de/test/antennapod/util/ConverterTest.java | 2 +- .../antennapod/util/FilenameGeneratorTest.java | 2 +- .../java/de/test/antennapod/util/URIUtilTest.java | 2 +- .../de/test/antennapod/util/URLCheckerTest.java | 2 +- .../antennapod/util/playback/TimelineTest.java | 10 +- .../util/syndication/FeedDiscovererTest.java | 2 +- .../syndication/feedgenerator/AtomGenerator.java | 8 +- .../syndication/feedgenerator/FeedGenerator.java | 2 +- .../syndication/feedgenerator/RSS2Generator.java | 6 +- app/src/main/AndroidManifest.xml | 20 +- .../main/java/de/danoeh/antennapod/PodcastApp.java | 8 +- .../antennapod/activity/AudioplayerActivity.java | 28 +- .../activity/DefaultOnlineFeedViewActivity.java | 16 +- .../activity/DirectoryChooserActivity.java | 2 +- .../activity/DownloadAuthenticationActivity.java | 6 +- .../antennapod/activity/FeedInfoActivity.java | 20 +- .../antennapod/activity/FlattrAuthActivity.java | 4 +- .../danoeh/antennapod/activity/MainActivity.java | 12 +- .../antennapod/activity/MediaplayerActivity.java | 27 +- .../activity/OnlineFeedViewActivity.java | 30 +- .../activity/OpmlFeedChooserActivity.java | 4 +- .../activity/OpmlImportBaseActivity.java | 6 +- .../activity/OpmlImportFromIntentActivity.java | 4 +- .../activity/OpmlImportFromPathActivity.java | 6 +- .../antennapod/activity/OpmlImportHolder.java | 2 +- .../antennapod/activity/PreferenceActivity.java | 14 +- .../antennapod/activity/StorageErrorActivity.java | 4 +- .../antennapod/activity/VideoplayerActivity.java | 10 +- .../gpoddernet/GpodnetAuthenticationActivity.java | 12 +- .../antennapod/adapter/ActionButtonCallback.java | 2 +- .../antennapod/adapter/ActionButtonUtils.java | 6 +- .../de/danoeh/antennapod/adapter/AdapterUtils.java | 6 +- .../antennapod/adapter/ChapterListAdapter.java | 8 +- .../adapter/DefaultActionButtonCallback.java | 14 +- .../antennapod/adapter/DownloadLogAdapter.java | 8 +- .../adapter/DownloadedEpisodesListAdapter.java | 6 +- .../antennapod/adapter/DownloadlistAdapter.java | 10 +- .../adapter/ExternalEpisodesListAdapter.java | 10 +- .../antennapod/adapter/FeedItemlistAdapter.java | 11 +- .../adapter/FeedItemlistDescriptionAdapter.java | 2 +- .../danoeh/antennapod/adapter/NavListAdapter.java | 4 +- .../antennapod/adapter/NewEpisodesListAdapter.java | 10 +- .../antennapod/adapter/QueueListAdapter.java | 8 +- .../antennapod/adapter/SearchlistAdapter.java | 10 +- .../adapter/gpodnet/PodcastListAdapter.java | 4 +- .../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 --- .../de/danoeh/antennapod/core/ClientConfig.java | 6 - .../antennapod/core/DownloadServiceCallbacks.java | 2 +- .../antennapod/core/PlaybackServiceCallbacks.java | 2 +- .../core/asynctask/DownloadObserver.java | 177 +++ .../antennapod/core/asynctask/FeedRemover.java | 74 ++ .../core/asynctask/FlattrClickWorker.java | 238 ++++ .../core/asynctask/FlattrStatusFetcher.java | 47 + .../core/asynctask/FlattrTokenFetcher.java | 95 ++ .../core/asynctask/OpmlExportWorker.java | 114 ++ .../antennapod/core/asynctask/OpmlFeedQueuer.java | 69 + .../core/asynctask/OpmlImportWorker.java | 116 ++ .../core/asynctask/PicassoImageResource.java | 37 + .../antennapod/core/asynctask/PicassoProvider.java | 152 +++ .../antennapod/core/backup/OpmlBackupAgent.java | 211 +++ .../antennapod/core/dialog/ConfirmationDialog.java | 64 + .../dialog/DownloadRequestErrorDialogCreator.java | 30 + .../danoeh/antennapod/core/dialog/TimeDialog.java | 138 ++ .../de/danoeh/antennapod/core/feed/Chapter.java | 55 + .../antennapod/core/feed/EventDistributor.java | 140 ++ .../java/de/danoeh/antennapod/core/feed/Feed.java | 445 +++++++ .../danoeh/antennapod/core/feed/FeedComponent.java | 66 + .../de/danoeh/antennapod/core/feed/FeedFile.java | 105 ++ .../de/danoeh/antennapod/core/feed/FeedImage.java | 71 + .../de/danoeh/antennapod/core/feed/FeedItem.java | 332 +++++ .../de/danoeh/antennapod/core/feed/FeedMedia.java | 408 ++++++ .../antennapod/core/feed/FeedPreferences.java | 89 ++ .../de/danoeh/antennapod/core/feed/ID3Chapter.java | 36 + .../de/danoeh/antennapod/core/feed/MediaType.java | 5 + .../danoeh/antennapod/core/feed/SearchResult.java | 34 + .../danoeh/antennapod/core/feed/SimpleChapter.java | 25 + .../antennapod/core/feed/VorbisCommentChapter.java | 109 ++ .../antennapod/core/gpoddernet/GpodnetService.java | 718 ++++++++++ .../GpodnetServiceAuthenticationException.java | 21 + .../GpodnetServiceBadStatusCodeException.java | 12 + .../core/gpoddernet/GpodnetServiceException.java | 19 + .../core/gpoddernet/model/GpodnetDevice.java | 72 + .../core/gpoddernet/model/GpodnetPodcast.java | 65 + .../model/GpodnetSubscriptionChange.java | 41 + .../core/gpoddernet/model/GpodnetTag.java | 46 + .../model/GpodnetUploadChangesResponse.java | 56 + .../danoeh/antennapod/core/opml/OpmlElement.java | 46 + .../de/danoeh/antennapod/core/opml/OpmlReader.java | 87 ++ .../danoeh/antennapod/core/opml/OpmlSymbols.java | 21 + .../de/danoeh/antennapod/core/opml/OpmlWriter.java | 65 + .../core/preferences/GpodnetPreferences.java | 246 ++++ .../core/preferences/PlaybackPreferences.java | 146 ++ .../core/preferences/UserPreferences.java | 577 ++++++++ .../core/receiver/AlarmUpdateReceiver.java | 33 + .../core/receiver/ConnectivityActionReceiver.java | 46 + .../core/receiver/FeedUpdateReceiver.java | 46 + .../core/receiver/MediaButtonReceiver.java | 32 + .../antennapod/core/receiver/PlayerWidget.java | 50 + .../core/service/GpodnetSyncService.java | 245 ++++ .../core/service/download/APRedirectHandler.java | 54 + .../service/download/AntennapodHttpClient.java | 96 ++ .../core/service/download/DownloadRequest.java | 209 +++ .../core/service/download/DownloadService.java | 1230 +++++++++++++++++ .../core/service/download/DownloadStatus.java | 181 +++ .../core/service/download/Downloader.java | 69 + .../core/service/download/DownloaderCallback.java | 10 + .../core/service/download/HttpDownloader.java | 246 ++++ .../core/service/playback/PlaybackService.java | 1080 +++++++++++++++ .../playback/PlaybackServiceMediaPlayer.java | 979 ++++++++++++++ .../playback/PlaybackServiceTaskManager.java | 384 ++++++ .../core/service/playback/PlayerStatus.java | 14 + .../core/service/playback/PlayerWidgetService.java | 190 +++ .../danoeh/antennapod/core/storage/DBReader.java | 908 +++++++++++++ .../de/danoeh/antennapod/core/storage/DBTasks.java | 895 +++++++++++++ .../danoeh/antennapod/core/storage/DBWriter.java | 974 ++++++++++++++ .../core/storage/DownloadRequestException.java | 25 + .../antennapod/core/storage/DownloadRequester.java | 366 +++++ .../core/storage/FeedItemStatistics.java | 70 + .../antennapod/core/storage/FeedSearcher.java | 57 + .../antennapod/core/storage/PodDBAdapter.java | 1391 ++++++++++++++++++++ .../core/syndication/handler/FeedHandler.java | 34 + .../syndication/handler/FeedHandlerResult.java | 19 + .../core/syndication/handler/HandlerState.java | 98 ++ .../core/syndication/handler/SyndHandler.java | 126 ++ .../core/syndication/handler/TypeGetter.java | 111 ++ .../handler/UnsupportedFeedtypeException.java | 38 + .../core/syndication/namespace/NSContent.java | 25 + .../core/syndication/namespace/NSITunes.java | 51 + .../core/syndication/namespace/NSMedia.java | 68 + .../core/syndication/namespace/NSRSS20.java | 141 ++ .../syndication/namespace/NSSimpleChapters.java | 42 + .../core/syndication/namespace/Namespace.java | 21 + .../core/syndication/namespace/SyndElement.java | 22 + .../core/syndication/namespace/atom/AtomText.java | 46 + .../core/syndication/namespace/atom/NSAtom.java | 194 +++ .../core/syndication/util/SyndDateUtils.java | 153 +++ .../core/syndication/util/SyndTypeUtils.java | 42 + .../danoeh/antennapod/core/util/ChapterUtils.java | 261 ++++ .../de/danoeh/antennapod/core/util/Converter.java | 103 ++ .../danoeh/antennapod/core/util/DownloadError.java | 52 + .../de/danoeh/antennapod/core/util/DuckType.java | 117 ++ .../danoeh/antennapod/core/util/EpisodeFilter.java | 49 + .../antennapod/core/util/FeedtitleComparator.java | 15 + .../antennapod/core/util/FileNameGenerator.java | 36 + .../antennapod/core/util/InvalidFeedException.java | 21 + .../de/danoeh/antennapod/core/util/LangUtils.java | 120 ++ .../danoeh/antennapod/core/util/NetworkUtils.java | 69 + .../danoeh/antennapod/core/util/QueueAccess.java | 93 ++ .../de/danoeh/antennapod/core/util/ShareUtils.java | 34 + .../antennapod/core/util/ShownotesProvider.java | 16 + .../danoeh/antennapod/core/util/StorageUtils.java | 66 + .../de/danoeh/antennapod/core/util/ThemeUtils.java | 22 + .../de/danoeh/antennapod/core/util/URIUtil.java | 35 + .../de/danoeh/antennapod/core/util/URLChecker.java | 51 + .../antennapod/core/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 + .../core/util/flattr/FlattrServiceCreator.java | 25 + .../antennapod/core/util/flattr/FlattrStatus.java | 68 + .../antennapod/core/util/flattr/FlattrThing.java | 7 + .../antennapod/core/util/flattr/FlattrUtils.java | 305 +++++ .../core/util/flattr/SimpleFlattrThing.java | 30 + .../core/util/gui/FeedItemUndoToken.java | 55 + .../core/util/id3reader/ChapterReader.java | 118 ++ .../antennapod/core/util/id3reader/ID3Reader.java | 250 ++++ .../core/util/id3reader/ID3ReaderException.java | 20 + .../core/util/id3reader/model/FrameHeader.java | 17 + .../core/util/id3reader/model/Header.java | 29 + .../core/util/id3reader/model/TagHeader.java | 26 + .../core/util/menuhandler/FeedItemMenuHandler.java | 191 +++ .../core/util/menuhandler/FeedMenuHandler.java | 86 ++ .../core/util/menuhandler/MenuItemUtils.java | 31 + .../core/util/menuhandler/NavDrawerActivity.java | 9 + .../antennapod/core/util/playback/AudioPlayer.java | 34 + .../core/util/playback/ExternalMedia.java | 235 ++++ .../antennapod/core/util/playback/IPlayer.java | 69 + .../core/util/playback/MediaPlayerError.java | 23 + .../antennapod/core/util/playback/Playable.java | 207 +++ .../core/util/playback/PlaybackController.java | 784 +++++++++++ .../antennapod/core/util/playback/Timeline.java | 161 +++ .../antennapod/core/util/playback/VideoPlayer.java | 67 + .../core/util/syndication/FeedDiscoverer.java | 78 ++ .../util/vorbiscommentreader/OggInputStream.java | 81 ++ .../VorbisCommentChapterReader.java | 101 ++ .../vorbiscommentreader/VorbisCommentHeader.java | 26 + .../vorbiscommentreader/VorbisCommentReader.java | 194 +++ .../VorbisCommentReaderException.java | 24 + .../dialog/AutoFlattrPreferenceDialog.java | 2 +- .../antennapod/dialog/ConfirmationDialog.java | 64 - .../dialog/DownloadRequestErrorDialogCreator.java | 30 - .../danoeh/antennapod/dialog/FeedItemDialog.java | 20 +- .../dialog/GpodnetSetHostnameDialog.java | 4 +- .../de/danoeh/antennapod/dialog/TimeDialog.java | 138 -- .../antennapod/dialog/VariableSpeedDialog.java | 2 +- .../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 -- .../fragment/CompletedDownloadsFragment.java | 10 +- .../danoeh/antennapod/fragment/CoverFragment.java | 4 +- .../antennapod/fragment/DownloadLogFragment.java | 6 +- .../fragment/ExternalPlayerFragment.java | 10 +- .../fragment/ItemDescriptionFragment.java | 18 +- .../antennapod/fragment/ItemlistFragment.java | 36 +- .../antennapod/fragment/NewEpisodesFragment.java | 30 +- .../fragment/PlaybackHistoryFragment.java | 20 +- .../danoeh/antennapod/fragment/QueueFragment.java | 20 +- .../fragment/RunningDownloadsFragment.java | 6 +- .../danoeh/antennapod/fragment/SearchFragment.java | 12 +- .../fragment/gpodnet/PodcastListFragment.java | 14 +- .../fragment/gpodnet/PodcastTopListFragment.java | 6 +- .../fragment/gpodnet/SearchListFragment.java | 10 +- .../fragment/gpodnet/SuggestionListFragment.java | 8 +- .../antennapod/fragment/gpodnet/TagFragment.java | 8 +- .../fragment/gpodnet/TagListFragment.java | 10 +- .../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 | 6 +- .../antennapod/service/GpodnetSyncService.java | 245 ---- .../service/download/APRedirectHandler.java | 54 - .../service/download/AntennapodHttpClient.java | 96 -- .../service/download/DownloadRequest.java | 209 --- .../service/download/DownloadService.java | 1231 ----------------- .../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 --- .../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 - 367 files changed, 21980 insertions(+), 22010 deletions(-) delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/DownloadObserver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FeedRemover.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FlattrClickWorker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/PicassoImageResource.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/PicassoProvider.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/backup/OpmlBackupAgent.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlExportWorker.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlFeedQueuer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlImportWorker.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/dialog/TimeDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/Feed.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerWidgetService.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/Converter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/DuckType.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedItemMenuHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedMenuHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/MenuItemUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/NavDrawerActivity.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/ConfirmationDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/Chapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/Feed.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedComponent.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedMedia.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/FeedPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/MediaType.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlElement.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlSymbols.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/GpodnetPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/PlaybackPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/APRedirectHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/AntennapodHttpClient.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloadRequest.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloadService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloadStatus.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/Downloader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/DownloaderCallback.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/download/HttpDownloader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlayerStatus.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/service/playback/PlayerWidgetService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DBReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DBTasks.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DBWriter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DownloadRequestException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/DownloadRequester.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/FeedItemStatistics.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/FeedSearcher.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/storage/PodDBAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/HandlerState.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/SyndHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/TypeGetter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSContent.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSITunes.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSMedia.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSRSS20.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/Namespace.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/SyndElement.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/util/SyndDateUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/ChapterUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/Converter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/DownloadError.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/DuckType.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/EpisodeFilter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/FeedtitleComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/FileNameGenerator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/InvalidFeedException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/LangUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/NetworkUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/QueueAccess.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/ShareUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/ShownotesProvider.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/StorageUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/ThemeUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/URIUtil.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/URLChecker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/UndoBarController.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrStatus.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrThing.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/ChapterReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3Reader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/model/Header.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/id3reader/model/TagHeader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/AudioPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/ExternalMedia.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/IPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/MediaPlayerError.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/Playable.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/PlaybackController.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/Timeline.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/playback/VideoPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java (limited to 'app') diff --git a/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java index 971a79a81..14a3b27b0 100644 --- a/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java +++ b/app/src/androidTest/java/de/test/antennapod/gpodnet/GPodnetServiceTest.java @@ -2,10 +2,10 @@ package de.test.antennapod.gpodnet; import android.test.AndroidTestCase; -import de.danoeh.antennapod.gpoddernet.GpodnetService; -import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice; -import de.danoeh.antennapod.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; import java.util.ArrayList; import java.util.Arrays; diff --git a/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java b/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java index 3fe42dfd0..52067c971 100644 --- a/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/handler/FeedHandlerTest.java @@ -2,9 +2,9 @@ package de.test.antennapod.handler; import android.content.Context; import android.test.InstrumentationTestCase; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.syndication.handler.FeedHandler; -import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; import de.test.antennapod.util.syndication.feedgenerator.AtomGenerator; import de.test.antennapod.util.syndication.feedgenerator.FeedGenerator; import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator; diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java index 575bbb998..1a561f282 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java @@ -2,12 +2,12 @@ package de.test.antennapod.service.download; import android.test.InstrumentationTestCase; import android.util.Log; -import de.danoeh.antennapod.feed.FeedFile; -import de.danoeh.antennapod.service.download.DownloadRequest; -import de.danoeh.antennapod.service.download.DownloadStatus; -import de.danoeh.antennapod.service.download.Downloader; -import de.danoeh.antennapod.service.download.HttpDownloader; -import de.danoeh.antennapod.util.DownloadError; +import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.service.download.HttpDownloader; +import de.danoeh.antennapod.core.util.DownloadError; import de.test.antennapod.util.service.download.HTTPBin; import java.io.File; diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java index b7d30a7b8..aac4c245a 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java @@ -3,13 +3,13 @@ package de.test.antennapod.service.playback; import android.content.Context; import android.media.RemoteControlClient; import android.test.InstrumentationTestCase; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.service.playback.PlayerStatus; -import de.danoeh.antennapod.storage.PodDBAdapter; -import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.playback.Playable; import de.test.antennapod.util.service.download.HTTPBin; import junit.framework.AssertionFailedError; import org.apache.commons.io.IOUtils; diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java index 86f609d74..81d684595 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java @@ -2,12 +2,12 @@ package de.test.antennapod.service.playback; import android.content.Context; import android.test.InstrumentationTestCase; -import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.service.playback.PlaybackServiceTaskManager; -import de.danoeh.antennapod.storage.PodDBAdapter; -import de.danoeh.antennapod.util.playback.Playable; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.playback.Playable; import java.util.ArrayList; import java.util.Date; diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java index fe337a5fb..63286d11d 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java @@ -2,14 +2,13 @@ package de.test.antennapod.storage; import android.content.Context; import android.test.InstrumentationTestCase; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.FeedItemStatistics; -import de.danoeh.antennapod.storage.PodDBAdapter; -import de.danoeh.antennapod.util.flattr.FlattrStatus; -import static de.test.antennapod.storage.DBTestUtils.*; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.FeedItemStatistics; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import java.util.ArrayList; import java.util.Date; diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java index 8859e50f9..fd5b1c393 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java @@ -4,14 +4,14 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.test.InstrumentationTestCase; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.PodDBAdapter; -import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import java.io.File; import java.io.IOException; diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java index 947984574..e7d6396f5 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java @@ -1,12 +1,12 @@ package de.test.antennapod.storage; import android.content.Context; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.storage.PodDBAdapter; -import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import junit.framework.Assert; import java.util.ArrayList; diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java index c1e045b79..4678a843b 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java @@ -4,13 +4,13 @@ import android.content.Context; import android.database.Cursor; import android.test.InstrumentationTestCase; import android.util.Log; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; import java.io.File; import java.io.IOException; diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java index 9b98c5f89..3656582e1 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -9,8 +9,8 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.PodDBAdapter; /** * User interface tests for MainActivity diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java index 28271a52b..daae4bd62 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java @@ -9,12 +9,12 @@ import com.robotium.solo.Solo; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AudioplayerActivity; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.playback.PlaybackService; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; import java.util.List; diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java index eac90e7c1..55fffb80a 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java @@ -6,8 +6,8 @@ import android.graphics.Bitmap; import android.os.Build; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.storage.PodDBAdapter; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.test.antennapod.util.service.download.HTTPBin; import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator; import junit.framework.Assert; diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java index e7782ff59..6c5a350de 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtilsTest.java @@ -1,8 +1,8 @@ package de.test.antennapod.ui; import android.test.InstrumentationTestCase; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; import org.apache.http.HttpStatus; import java.io.File; diff --git a/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java index 9ab444a99..47fca41ba 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/ConverterTest.java @@ -2,7 +2,7 @@ package de.test.antennapod.util; import android.test.AndroidTestCase; -import de.danoeh.antennapod.util.Converter; +import de.danoeh.antennapod.core.util.Converter; /** * Test class for converter diff --git a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java b/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java index bf7f57459..6d24fa526 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java @@ -3,7 +3,7 @@ package de.test.antennapod.util; import java.io.File; import java.io.IOException; -import de.danoeh.antennapod.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.FileNameGenerator; import android.test.AndroidTestCase; public class FilenameGeneratorTest extends AndroidTestCase { diff --git a/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java b/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java index 61d230295..7bdcfb898 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/URIUtilTest.java @@ -1,7 +1,7 @@ package de.test.antennapod.util; import android.test.AndroidTestCase; -import de.danoeh.antennapod.util.URIUtil; +import de.danoeh.antennapod.core.util.URIUtil; /** * Test class for URIUtil diff --git a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java b/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java index fd485e0da..d1bfb592a 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java @@ -1,7 +1,7 @@ package de.test.antennapod.util; import android.test.AndroidTestCase; -import de.danoeh.antennapod.util.URLChecker; +import de.danoeh.antennapod.core.util.URLChecker; /** * Test class for URLChecker diff --git a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java index c2b7ad316..2c56b71cc 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java @@ -11,11 +11,11 @@ import org.jsoup.select.Elements; import java.util.Date; import java.util.List; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.util.playback.Playable; -import de.danoeh.antennapod.util.playback.Timeline; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.Timeline; /** * Test class for timeline diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java index 10414fd2a..4e5d0297f 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java @@ -1,7 +1,7 @@ package de.test.antennapod.util.syndication; import android.test.InstrumentationTestCase; -import de.danoeh.antennapod.util.syndication.FeedDiscoverer; +import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java index 8ce6c08e4..69cc827ec 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java @@ -1,10 +1,10 @@ package de.test.antennapod.util.syndication.feedgenerator; import android.util.Xml; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java index 23518de87..fe5afd847 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java @@ -1,6 +1,6 @@ package de.test.antennapod.util.syndication.feedgenerator; -import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.core.feed.Feed; import java.io.IOException; import java.io.OutputStream; diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java index 6355d4bb9..d37434f06 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java @@ -1,9 +1,9 @@ package de.test.antennapod.util.syndication.feedgenerator; import android.util.Xml; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65eac99ea..768a4abb0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,7 +35,7 @@ android:name="de.danoeh.antennapod.PodcastApp" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:backupAgent=".backup.OpmlBackupAgent" + android:backupAgent=".core.backup.OpmlBackupAgent" android:restoreAnyVersion="true" android:logo="@drawable/ic_launcher" android:theme="@style/Theme.AntennaPod.Light"> @@ -75,16 +75,16 @@ android:launchMode="singleInstance"/> @@ -98,7 +98,7 @@ @@ -112,12 +112,12 @@ - + @@ -133,7 +133,7 @@ - + @@ -324,12 +324,12 @@ - + - + diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java index 74628f3d6..25aa9fb68 100644 --- a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java +++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java @@ -2,10 +2,10 @@ 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.core.feed.EventDistributor; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.spa.SPAUtil; /** Main application class. */ diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java index 18d27ddda..5622bc987 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -31,23 +31,23 @@ 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.core.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.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.feed.SimpleChapter; import de.danoeh.antennapod.fragment.CoverFragment; import de.danoeh.antennapod.fragment.ItemDescriptionFragment; -import de.danoeh.antennapod.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; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.core.util.playback.ExternalMedia; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; /** * Activity for playing audio files. diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java index a03fa7949..239fe1893 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java @@ -30,14 +30,14 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; /** * Default implementation of OnlineFeedViewActivity. Shows the downloaded feed's items with their descriptions, diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java index 06a11c775..559fa0574 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DirectoryChooserActivity.java @@ -19,7 +19,7 @@ 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 de.danoeh.antennapod.core.preferences.UserPreferences; import java.io.File; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java index c5f25d813..365c4216d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java @@ -14,9 +14,9 @@ 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; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.storage.DownloadRequester; /** * Shows a username and a password text field. diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java index 5cf187eb6..80484df37 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -12,16 +12,16 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.util.LangUtils; +import de.danoeh.antennapod.core.util.menuhandler.FeedMenuHandler; /** * Displays information about a feed. diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java index 8dde14d3b..f4a973fac 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java @@ -14,8 +14,8 @@ 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 de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; import org.shredzone.flattr4j.exception.FlattrException; /** Guides the user through the authentication process */ diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index b7014dab2..2c660019c 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -25,13 +25,13 @@ 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.core.feed.EventDistributor; +import de.danoeh.antennapod.core.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 de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java index 2e5372b60..249a3c5c3 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -3,13 +3,11 @@ 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; @@ -24,19 +22,18 @@ 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; +import de.danoeh.antennapod.core.dialog.TimeDialog; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.playback.MediaPlayerError; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; /** * Provides general features which are both needed for playing audio and video diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 2c6d75cd8..d84e6cc03 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -16,21 +16,21 @@ 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 de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.service.download.HttpDownloader; +import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; +import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.URLChecker; +import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; import org.apache.commons.lang3.StringUtils; import org.xml.sax.SAXException; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java index e09941abf..8a9ec58d3 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java @@ -13,8 +13,8 @@ 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 de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.preferences.UserPreferences; import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java index d3fd3949c..2e66978fd 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java @@ -4,9 +4,9 @@ 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 de.danoeh.antennapod.core.asynctask.OpmlFeedQueuer; +import de.danoeh.antennapod.core.asynctask.OpmlImportWorker; +import de.danoeh.antennapod.core.opml.OpmlElement; import java.io.Reader; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java index 16e663fac..e42072ead 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromIntentActivity.java @@ -2,8 +2,8 @@ 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 de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.LangUtils; import java.io.BufferedReader; import java.io.InputStreamReader; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java index 631e69f78..162a8f2e5 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java @@ -13,9 +13,9 @@ 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 de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.LangUtils; +import de.danoeh.antennapod.core.util.StorageUtils; import java.io.*; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java index ec53ed7b6..7afa270cc 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportHolder.java @@ -1,6 +1,6 @@ package de.danoeh.antennapod.activity; -import de.danoeh.antennapod.opml.OpmlElement; +import de.danoeh.antennapod.core.opml.OpmlElement; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java index cd6731c02..65efcc230 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -21,17 +21,17 @@ 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.core.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.core.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 de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; import java.io.File; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java index d8a137eb9..173bec6b2 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java @@ -12,8 +12,8 @@ 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; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.StorageUtils; /** Is show if there is now external storage available. */ public class StorageErrorActivity extends ActionBarActivity { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java index 81661a288..d3df40afb 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -21,11 +21,11 @@ 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.core.feed.MediaType; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.playback.ExternalMedia; +import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.view.AspectRatioVideoView; /** 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 index 6a60f65fe..d7b069b19 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -15,12 +15,12 @@ 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 de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.GpodnetSyncService; import java.security.SecureRandom; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java index 30ad2d03f..a75789815 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java @@ -1,6 +1,6 @@ package de.danoeh.antennapod.adapter; -import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedItem; public interface ActionButtonCallback { /** Is called when the action button of a list item has been pressed. */ diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java index 1de071a73..fecddeaf4 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java @@ -8,9 +8,9 @@ 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; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; /** * Utility methods for the action button that is displayed on the right hand side diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java index f393fb7d7..c3902639a 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java @@ -5,9 +5,9 @@ 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; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.util.Converter; /** * Utility methods for adapters diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java index c12de6ebd..9e59a2a1a 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChapterListAdapter.java @@ -16,10 +16,10 @@ 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 de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java index 0c4cbe685..800462023 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java @@ -6,13 +6,13 @@ 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; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; /** * Default implementation of an ActionButtonCallback diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index 2cc216227..f982e86ce 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -8,10 +8,10 @@ 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; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.DownloadStatus; /** Displays a list of DownloadStatus entries. */ public class DownloadLogAdapter extends BaseAdapter { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java index ef5af67de..8785916a0 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadedEpisodesListAdapter.java @@ -11,9 +11,9 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.util.Converter; /** * Shows a list of downloaded episodes diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java index 658af9e4e..4257c6eb9 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -9,11 +9,11 @@ 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; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ThemeUtils; public class DownloadlistAdapter extends BaseAdapter { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java index 3f666eb8b..8b1ed9112 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java @@ -13,11 +13,11 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.Converter; /** * Displays unread items and items in the queue in one combined list. The diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java index 357b5f8b4..2f69e6580 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java @@ -9,12 +9,11 @@ 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; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.ThemeUtils; /** * List adapter for items of feeds that the user has already subscribed to. diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index c2c2285ac..9011c8b02 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -7,7 +7,7 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedItem; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java index ef8e8ce07..a917633e6 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -11,8 +11,8 @@ 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.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.Feed; /** * BaseAdapter for the navigation drawer diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java index 8abe49133..a0829286c 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java @@ -12,11 +12,11 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.Converter; /** * List adapter for the list of new episodes diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java index ebe519592..bc42ad063 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java @@ -6,10 +6,10 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; /** * List adapter for the queue. diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java index 2314c2269..79c1f6f99 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SearchlistAdapter.java @@ -9,11 +9,11 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedComponent; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.SearchResult; /** * List adapter for search activity. 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 index f2e78a57e..9063d35c9 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -11,8 +11,8 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; /** * Adapter for displaying a list of GPodnetPodcast-Objects. diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/DownloadObserver.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DownloadObserver.java deleted file mode 100644 index 21ae5291e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/DownloadObserver.java +++ /dev/null @@ -1,177 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.app.Activity; -import android.content.*; -import android.os.Handler; -import android.os.IBinder; -import android.util.Log; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.service.download.DownloadService; -import de.danoeh.antennapod.service.download.Downloader; - -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Provides access to the DownloadService's list of items that are currently being downloaded. - * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause() - * should be called in the activity's onResume() and onPause() methods - */ -public class DownloadObserver { - private static final String TAG = "DownloadObserver"; - - /** - * Time period between update notifications. - */ - public static final int WAITING_INTERVAL_MS = 3000; - - private volatile Activity activity; - private final Handler handler; - private final Callback callback; - - private DownloadService downloadService = null; - private AtomicBoolean mIsBound = new AtomicBoolean(false); - - private Thread refresherThread; - private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false); - - - /** - * Creates a new download observer. - * - * @param activity Used for registering receivers - * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread. - * @param callback Callback methods for posting content updates - * @throws java.lang.IllegalArgumentException if one of the arguments is null. - */ - public DownloadObserver(Activity activity, Handler handler, Callback callback) { - Validate.notNull(activity); - Validate.notNull(handler); - Validate.notNull(callback); - - this.activity = activity; - this.handler = handler; - this.callback = callback; - } - - public void onResume() { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed"); - activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); - connectToDownloadService(); - } - - public void onPause() { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver paused"); - try { - activity.unregisterReceiver(contentChangedReceiver); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - try { - activity.unbindService(mConnection); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - stopRefresher(); - } - - private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // reconnect to DownloadService if connection has been closed - if (downloadService == null) { - connectToDownloadService(); - } - callback.onContentChanged(); - startRefresher(); - } - }; - - public interface Callback { - void onContentChanged(); - - void onDownloadDataAvailable(List 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 deleted file mode 100644 index 0549a4255..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/FeedRemover.java +++ /dev/null @@ -1,74 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.annotation.SuppressLint; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.os.AsyncTask; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.storage.DBWriter; - -import java.util.concurrent.ExecutionException; - -/** Removes a feed in the background. */ -public class FeedRemover extends AsyncTask { - 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 deleted file mode 100644 index 9210ac1d1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrClickWorker.java +++ /dev/null @@ -1,238 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.annotation.TargetApi; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.widget.Toast; - -import org.apache.commons.lang3.Validate; -import org.shredzone.flattr4j.exception.FlattrException; - -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.FlattrAuthActivity; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.NetworkUtils; -import de.danoeh.antennapod.util.flattr.FlattrThing; -import de.danoeh.antennapod.util.flattr.FlattrUtils; - -/** - * Performs a click action in a background thread. - *

- * 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 deleted file mode 100644 index 04d349671..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrStatusFetcher.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.content.Context; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.flattr.FlattrUtils; -import org.shredzone.flattr4j.exception.FlattrException; -import org.shredzone.flattr4j.model.Flattr; - -import java.util.List; -import java.util.concurrent.ExecutionException; - -/** - * Fetch list of flattred things and flattr status in database in a background thread. - */ - -public class FlattrStatusFetcher extends Thread { - protected static final String TAG = "FlattrStatusFetcher"; - protected Context context; - - public FlattrStatusFetcher(Context context) { - super(); - this.context = context; - } - - @Override - public void run() { - if (BuildConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status"); - - Thread.currentThread().setPriority(Thread.MIN_PRIORITY); - - try { - List 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 deleted file mode 100644 index 0dcf832f7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/FlattrTokenFetcher.java +++ /dev/null @@ -1,95 +0,0 @@ -package de.danoeh.antennapod.asynctask; - - -import android.annotation.SuppressLint; -import android.app.ProgressDialog; -import android.content.Context; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.FlattrAuthActivity; -import de.danoeh.antennapod.util.flattr.FlattrUtils; -import org.shredzone.flattr4j.exception.FlattrException; -import org.shredzone.flattr4j.oauth.AccessToken; -import org.shredzone.flattr4j.oauth.AndroidAuthenticator; - -/** Fetches the access token in the background in order to avoid networkOnMainThread exception. */ - -public class FlattrTokenFetcher extends AsyncTask { - 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 deleted file mode 100644 index 4abb1a67d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java +++ /dev/null @@ -1,114 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.annotation.SuppressLint; -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.AsyncTask; -import android.util.Log; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.opml.OpmlWriter; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.util.LangUtils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; - -/** Writes an OPML file into the export directory in the background. */ -public class OpmlExportWorker extends AsyncTask { - 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 deleted file mode 100644 index 038b8dcc5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.annotation.SuppressLint; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.OpmlImportHolder; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.opml.OpmlElement; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; - -import java.util.Arrays; -import java.util.Date; - -/** Queues items for download in the background. */ -public class OpmlFeedQueuer extends AsyncTask { - 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 deleted file mode 100644 index 13534fa64..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java +++ /dev/null @@ -1,116 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.annotation.SuppressLint; -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.os.AsyncTask; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.opml.OpmlElement; -import de.danoeh.antennapod.opml.OpmlReader; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; - -public class OpmlImportWorker extends - AsyncTask> { - 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 deleted file mode 100644 index 26f9d9278..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/PicassoImageResource.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.net.Uri; - -/** - * Classes that implement this interface provide access to an image resource that can - * be loaded by the Picasso library. - */ -public interface PicassoImageResource { - - /** - * This scheme should be used by PicassoImageResources to - * indicate that the image Uri points to a file that is not an image - * (e.g. a media file). This workaround is needed so that the Picasso library - * loads these Uri with a Downloader instead of trying to load it directly. - *

- * 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 deleted file mode 100644 index 849725630..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/PicassoProvider.java +++ /dev/null @@ -1,152 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import android.content.Context; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import com.squareup.picasso.Cache; -import com.squareup.picasso.Downloader; -import com.squareup.picasso.LruCache; -import com.squareup.picasso.OkHttpDownloader; -import com.squareup.picasso.Picasso; - -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * Provides access to Picasso instances. - */ -public class PicassoProvider { - private static final String TAG = "PicassoProvider"; - - private static final boolean DEBUG = false; - - private static ExecutorService executorService; - private static Cache memoryCache; - - private static Picasso defaultPicassoInstance; - private static Picasso mediaMetadataPicassoInstance; - - private static synchronized ExecutorService getExecutorService() { - if (executorService == null) { - executorService = Executors.newFixedThreadPool(3); - } - return executorService; - } - - private static synchronized Cache getMemoryCache(Context context) { - if (memoryCache == null) { - memoryCache = new LruCache(context); - } - return memoryCache; - } - - /** - * Returns a Picasso instance that uses an OkHttpDownloader. This instance can only load images - * from image files. - *

- * 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 deleted file mode 100644 index 56d1ca092..000000000 --- a/app/src/main/java/de/danoeh/antennapod/backup/OpmlBackupAgent.java +++ /dev/null @@ -1,212 +0,0 @@ -package de.danoeh.antennapod.backup; - -import android.app.backup.BackupAgentHelper; -import android.app.backup.BackupDataInputStream; -import android.app.backup.BackupDataOutput; -import android.app.backup.BackupHelper; -import android.content.Context; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import de.danoeh.antennapod.BuildConfig; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.math.BigInteger; -import java.security.DigestInputStream; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; - -import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.opml.OpmlElement; -import de.danoeh.antennapod.opml.OpmlReader; -import de.danoeh.antennapod.opml.OpmlWriter; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.util.LangUtils; - -public class OpmlBackupAgent extends BackupAgentHelper { - private static final String OPML_BACKUP_KEY = "opml"; - - @Override - public void onCreate() { - addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this)); - } - - private static final void LOGD(String tag, String msg) { - if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { - Log.d(tag, msg); - } - } - - private static final void LOGD(String tag, String msg, Throwable tr) { - if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { - Log.d(tag, msg, tr); - } - } - - /** Class for backing up and restoring the OPML file. */ - private static class OpmlBackupHelper implements BackupHelper { - private static final String TAG = "OpmlBackupHelper"; - - private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml"; - - private final Context mContext; - - /** Checksum of restored OPML file */ - private byte[] mChecksum; - - public OpmlBackupHelper(Context context) { - mContext = context; - } - - @Override - public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { - Log.d(TAG, "Performing backup"); - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - MessageDigest digester = null; - Writer writer; - - try { - digester = MessageDigest.getInstance("MD5"); - writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester), - LangUtils.UTF_8); - } catch (NoSuchAlgorithmException e) { - writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8); - } - - try { - // Write OPML - new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer); - - // Compare checksum of new and old file to see if we need to perform a backup at all - if (digester != null) { - byte[] newChecksum = digester.digest(); - LOGD(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16)); - - // Get the old checksum - if (oldState != null) { - FileInputStream inState = new FileInputStream(oldState.getFileDescriptor()); - int len = inState.read(); - - if (len != -1) { - byte[] oldChecksum = new byte[len]; - inState.read(oldChecksum); - LOGD(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16)); - - if (Arrays.equals(oldChecksum, newChecksum)) { - LOGD(TAG, "Checksums are the same; won't backup"); - return; - } - } - } - - writeNewStateDescription(newState, newChecksum); - } - - LOGD(TAG, "Backing up OPML"); - byte[] bytes = byteStream.toByteArray(); - data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length); - data.writeEntityData(bytes, bytes.length); - } catch (IOException e) { - Log.e(TAG, "Error during backup", e); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException e) { - } - } - } - } - - @Override - public void restoreEntity(BackupDataInputStream data) { - LOGD(TAG, "Backup restore"); - - if (!OPML_ENTITY_KEY.equals(data.getKey())) { - LOGD(TAG, "Unknown entity key: " + data.getKey()); - return; - } - - MessageDigest digester = null; - Reader reader; - - try { - digester = MessageDigest.getInstance("MD5"); - reader = new InputStreamReader(new DigestInputStream(data, digester), - LangUtils.UTF_8); - } catch (NoSuchAlgorithmException e) { - reader = new InputStreamReader(data, LangUtils.UTF_8); - } - - try { - ArrayList 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/core/ClientConfig.java b/app/src/main/java/de/danoeh/antennapod/core/ClientConfig.java index 72ebe6347..bf28c17ea 100644 --- a/app/src/main/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/app/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -6,12 +6,6 @@ package de.danoeh.antennapod.core; */ public class ClientConfig { - /** - * Package name of the client. This string is used as a prefix - * for internal intents. - */ - public static String CLIENT_PACKAGE_NAME; - /** * Should be used when setting User-Agent header for HTTP-requests. */ diff --git a/app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java b/app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java index 1b0c4aab4..9e4ed8e2b 100644 --- a/app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java +++ b/app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core; import android.app.PendingIntent; -import de.danoeh.antennapod.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadRequest; /** * Callbacks for the DownloadService of the core module diff --git a/app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java index da64b38ad..a74c441c4 100644 --- a/app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java +++ b/app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core; import android.content.Intent; -import de.danoeh.antennapod.feed.MediaType; +import de.danoeh.antennapod.core.feed.MediaType; /** * Callbacks for the PlaybackService of the core module diff --git a/app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java new file mode 100644 index 000000000..8b3635af8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java @@ -0,0 +1,177 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.app.Activity; +import android.content.*; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provides access to the DownloadService's list of items that are currently being downloaded. + * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause() + * should be called in the activity's onResume() and onPause() methods + */ +public class DownloadObserver { + private static final String TAG = "DownloadObserver"; + + /** + * Time period between update notifications. + */ + public static final int WAITING_INTERVAL_MS = 3000; + + private volatile Activity activity; + private final Handler handler; + private final Callback callback; + + private DownloadService downloadService = null; + private AtomicBoolean mIsBound = new AtomicBoolean(false); + + private Thread refresherThread; + private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false); + + + /** + * Creates a new download observer. + * + * @param activity Used for registering receivers + * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread. + * @param callback Callback methods for posting content updates + * @throws java.lang.IllegalArgumentException if one of the arguments is null. + */ + public DownloadObserver(Activity activity, Handler handler, Callback callback) { + Validate.notNull(activity); + Validate.notNull(handler); + Validate.notNull(callback); + + this.activity = activity; + this.handler = handler; + this.callback = callback; + } + + public void onResume() { + if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed"); + activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); + connectToDownloadService(); + } + + public void onPause() { + if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver paused"); + try { + activity.unregisterReceiver(contentChangedReceiver); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + try { + activity.unbindService(mConnection); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + stopRefresher(); + } + + private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // reconnect to DownloadService if connection has been closed + if (downloadService == null) { + connectToDownloadService(); + } + callback.onContentChanged(); + startRefresher(); + } + }; + + public interface Callback { + void onContentChanged(); + + void onDownloadDataAvailable(List 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/core/asynctask/FeedRemover.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java new file mode 100644 index 000000000..2201dfbe7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java @@ -0,0 +1,74 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.os.AsyncTask; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.DBWriter; + +import java.util.concurrent.ExecutionException; + +/** Removes a feed in the background. */ +public class FeedRemover extends AsyncTask { + 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/core/asynctask/FlattrClickWorker.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java new file mode 100644 index 000000000..44ad91981 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java @@ -0,0 +1,238 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.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.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; + +/** + * Performs a click action in a background thread. + *

+ * 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/core/asynctask/FlattrStatusFetcher.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java new file mode 100644 index 000000000..ddc4370e6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Fetch list of flattred things and flattr status in database in a background thread. + */ + +public class FlattrStatusFetcher extends Thread { + protected static final String TAG = "FlattrStatusFetcher"; + protected Context context; + + public FlattrStatusFetcher(Context context) { + super(); + this.context = context; + } + + @Override + public void run() { + if (BuildConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status"); + + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + + try { + List 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/core/asynctask/FlattrTokenFetcher.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java new file mode 100644 index 000000000..6f8319c7d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java @@ -0,0 +1,95 @@ +package de.danoeh.antennapod.core.asynctask; + + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.FlattrAuthActivity; +import de.danoeh.antennapod.core.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/core/asynctask/OpmlExportWorker.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlExportWorker.java new file mode 100644 index 000000000..9f887bda6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlExportWorker.java @@ -0,0 +1,114 @@ +package de.danoeh.antennapod.core.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.core.opml.OpmlWriter; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.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/core/asynctask/OpmlFeedQueuer.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlFeedQueuer.java new file mode 100644 index 000000000..13144faa9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlFeedQueuer.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.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.core.feed.Feed; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +import java.util.Arrays; +import java.util.Date; + +/** Queues items for download in the background. */ +public class OpmlFeedQueuer extends AsyncTask { + 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/core/asynctask/OpmlImportWorker.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlImportWorker.java new file mode 100644 index 000000000..a4308be9b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlImportWorker.java @@ -0,0 +1,116 @@ +package de.danoeh.antennapod.core.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.core.opml.OpmlElement; +import de.danoeh.antennapod.core.opml.OpmlReader; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; + +public class OpmlImportWorker extends + AsyncTask> { + 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/core/asynctask/PicassoImageResource.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java new file mode 100644 index 000000000..c0d8049db --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.net.Uri; + +/** + * Classes that implement this interface provide access to an image resource that can + * be loaded by the Picasso library. + */ +public interface PicassoImageResource { + + /** + * This scheme should be used by PicassoImageResources to + * indicate that the image Uri points to a file that is not an image + * (e.g. a media file). This workaround is needed so that the Picasso library + * loads these Uri with a Downloader instead of trying to load it directly. + *

+ * 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/core/asynctask/PicassoProvider.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java new file mode 100644 index 000000000..6ace92800 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java @@ -0,0 +1,152 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.content.Context; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import com.squareup.picasso.Cache; +import com.squareup.picasso.Downloader; +import com.squareup.picasso.LruCache; +import com.squareup.picasso.OkHttpDownloader; +import com.squareup.picasso.Picasso; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Provides access to Picasso instances. + */ +public class PicassoProvider { + private static final String TAG = "PicassoProvider"; + + private static final boolean DEBUG = false; + + private static ExecutorService executorService; + private static Cache memoryCache; + + private static Picasso defaultPicassoInstance; + private static Picasso mediaMetadataPicassoInstance; + + private static synchronized ExecutorService getExecutorService() { + if (executorService == null) { + executorService = Executors.newFixedThreadPool(3); + } + return executorService; + } + + private static synchronized Cache getMemoryCache(Context context) { + if (memoryCache == null) { + memoryCache = new LruCache(context); + } + return memoryCache; + } + + /** + * Returns a Picasso instance that uses an OkHttpDownloader. This instance can only load images + * from image files. + *

+ * 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/core/backup/OpmlBackupAgent.java b/app/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java new file mode 100644 index 000000000..72b5066b3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -0,0 +1,211 @@ +package de.danoeh.antennapod.core.backup; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInputStream; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupHelper; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import de.danoeh.antennapod.BuildConfig; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.math.BigInteger; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.opml.OpmlReader; +import de.danoeh.antennapod.core.opml.OpmlWriter; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LangUtils; + +public class OpmlBackupAgent extends BackupAgentHelper { + private static final String OPML_BACKUP_KEY = "opml"; + + @Override + public void onCreate() { + addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this)); + } + + private static final void LOGD(String tag, String msg) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg); + } + } + + private static final void LOGD(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg, tr); + } + } + + /** Class for backing up and restoring the OPML file. */ + private static class OpmlBackupHelper implements BackupHelper { + private static final String TAG = "OpmlBackupHelper"; + + private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml"; + + private final Context mContext; + + /** Checksum of restored OPML file */ + private byte[] mChecksum; + + public OpmlBackupHelper(Context context) { + mContext = context; + } + + @Override + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { + Log.d(TAG, "Performing backup"); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + MessageDigest digester = null; + Writer writer; + + try { + digester = MessageDigest.getInstance("MD5"); + writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8); + } + + try { + // Write OPML + new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer); + + // Compare checksum of new and old file to see if we need to perform a backup at all + if (digester != null) { + byte[] newChecksum = digester.digest(); + LOGD(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16)); + + // Get the old checksum + if (oldState != null) { + FileInputStream inState = new FileInputStream(oldState.getFileDescriptor()); + int len = inState.read(); + + if (len != -1) { + byte[] oldChecksum = new byte[len]; + inState.read(oldChecksum); + LOGD(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16)); + + if (Arrays.equals(oldChecksum, newChecksum)) { + LOGD(TAG, "Checksums are the same; won't backup"); + return; + } + } + } + + writeNewStateDescription(newState, newChecksum); + } + + LOGD(TAG, "Backing up OPML"); + byte[] bytes = byteStream.toByteArray(); + data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length); + data.writeEntityData(bytes, bytes.length); + } catch (IOException e) { + Log.e(TAG, "Error during backup", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void restoreEntity(BackupDataInputStream data) { + LOGD(TAG, "Backup restore"); + + if (!OPML_ENTITY_KEY.equals(data.getKey())) { + LOGD(TAG, "Unknown entity key: " + data.getKey()); + return; + } + + MessageDigest digester = null; + Reader reader; + + try { + digester = MessageDigest.getInstance("MD5"); + reader = new InputStreamReader(new DigestInputStream(data, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + reader = new InputStreamReader(data, LangUtils.UTF_8); + } + + try { + ArrayList 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/core/dialog/ConfirmationDialog.java b/app/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java new file mode 100644 index 000000000..e51d70708 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.core.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; +import de.danoeh.antennapod.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/core/dialog/DownloadRequestErrorDialogCreator.java b/app/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java new file mode 100644 index 000000000..a1c3a4c6a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import de.danoeh.antennapod.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/core/dialog/TimeDialog.java b/app/src/main/java/de/danoeh/antennapod/core/dialog/TimeDialog.java new file mode 100644 index 000000000..a95e8c6c5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/dialog/TimeDialog.java @@ -0,0 +1,138 @@ +package de.danoeh.antennapod.core.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/core/feed/Chapter.java b/app/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java new file mode 100644 index 000000000..ce3352ed6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.core.feed; + +public abstract class Chapter extends FeedComponent { + + /** Defines starting point in milliseconds. */ + protected long start; + protected String title; + protected String link; + + public Chapter() { + } + + public Chapter(long start) { + super(); + this.start = start; + } + + public Chapter(long start, String title, FeedItem item, String link) { + super(); + this.start = start; + this.title = title; + this.link = link; + } + + public abstract int getChapterType(); + + public long getStart() { + return start; + } + + public String getTitle() { + return title; + } + + public String getLink() { + return link; + } + + public void setStart(long start) { + this.start = start; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setLink(String link) { + this.link = link; + } + + @Override + public String getHumanReadableIdentifier() { + return title; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java b/app/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java new file mode 100644 index 000000000..65c55a361 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -0,0 +1,140 @@ +package de.danoeh.antennapod.core.feed; + +import android.os.Handler; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.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/core/feed/Feed.java b/app/src/main/java/de/danoeh/antennapod/core/feed/Feed.java new file mode 100644 index 000000000..3f83ab8b6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -0,0 +1,445 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.Context; +import android.net.Uri; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.EpisodeFilter; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Data Object for a whole feed + * + * @author daniel + */ +public class Feed extends FeedFile implements FlattrThing, PicassoImageResource { + public static final int FEEDFILETYPE_FEED = 0; + public static final String TYPE_RSS2 = "rss"; + public static final String TYPE_RSS091 = "rss"; + public static final String TYPE_ATOM1 = "atom"; + + private String title; + /** + * Contains 'id'-element in Atom feed. + */ + private String feedIdentifier; + /** + * Link to the website. + */ + private String link; + private String description; + private String language; + /** + * Name of the author + */ + private String author; + private FeedImage image; + private List 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/core/feed/FeedComponent.java b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java new file mode 100644 index 000000000..05115c1ea --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java @@ -0,0 +1,66 @@ +package de.danoeh.antennapod.core.feed; + +/** + * Represents every possible component of a feed + * + * @author daniel + */ +public abstract class FeedComponent { + + protected long id; + + public FeedComponent() { + super(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + /** + * Update this FeedComponent's attributes with the attributes from another + * FeedComponent. This method should only update attributes which where read from + * the feed. + */ + public void updateFromOther(FeedComponent other) { + } + + /** + * Compare's this FeedComponent's attribute values with another FeedComponent's + * attribute values. This method will only compare attributes which were + * read from the feed. + * + * @return true if attribute values are different, false otherwise + */ + public boolean compareWithOther(FeedComponent other) { + return false; + } + + + /** + * Should return a non-null, human-readable String so that the item can be + * identified by the user. Can be title, download-url, etc. + */ + public abstract String getHumanReadableIdentifier(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FeedComponent that = (FeedComponent) o; + + if (id != that.id) return false; + + return true; + } + + @Override + public int hashCode() { + return (int) (id ^ (id >>> 32)); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java new file mode 100644 index 000000000..3dc58654b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java @@ -0,0 +1,105 @@ +package de.danoeh.antennapod.core.feed; + +import java.io.File; + +/** + * Represents a component of a Feed that has to be downloaded + */ +public abstract class FeedFile extends FeedComponent { + + protected String file_url; + protected String download_url; + protected boolean downloaded; + + /** + * Creates a new FeedFile object. + * + * @param file_url The location of the FeedFile. If this is null, the downloaded-attribute + * will automatically be set to false. + * @param download_url The location where the FeedFile can be downloaded. + * @param downloaded true if the FeedFile has been downloaded, false otherwise. This parameter + * will automatically be interpreted as false if the file_url is null. + */ + public FeedFile(String file_url, String download_url, boolean downloaded) { + super(); + this.file_url = file_url; + this.download_url = download_url; + this.downloaded = (file_url != null) && downloaded; + } + + public FeedFile() { + this(null, null, false); + } + + public abstract int getTypeAsInt(); + + /** + * Update this FeedFile's attributes with the attributes from another + * FeedFile. This method should only update attributes which where read from + * the feed. + */ + public void updateFromOther(FeedFile other) { + super.updateFromOther(other); + this.download_url = other.download_url; + } + + /** + * Compare's this FeedFile's attribute values with another FeedFile's + * attribute values. This method will only compare attributes which were + * read from the feed. + * + * @return true if attribute values are different, false otherwise + */ + public boolean compareWithOther(FeedFile other) { + if (super.compareWithOther(other)) { + return true; + } + if (!download_url.equals(other.download_url)) { + return true; + } + return false; + } + + /** + * Returns true if the file exists at file_url. + */ + public boolean fileExists() { + if (file_url == null) { + return false; + } else { + File f = new File(file_url); + return f.exists(); + } + } + + public String getFile_url() { + return file_url; + } + + /** + * Changes the file_url of this FeedFile. Setting this value to + * null will also set the downloaded-attribute to false. + */ + public void setFile_url(String file_url) { + this.file_url = file_url; + if (file_url == null) { + downloaded = false; + } + } + + public String getDownload_url() { + return download_url; + } + + public void setDownload_url(String download_url) { + this.download_url = download_url; + } + + public boolean isDownloaded() { + return downloaded; + } + + public void setDownloaded(boolean downloaded) { + this.downloaded = downloaded; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java new file mode 100644 index 000000000..51605691d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java @@ -0,0 +1,71 @@ +package de.danoeh.antennapod.core.feed; + +import android.net.Uri; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; + +import java.io.File; + + +public class FeedImage extends FeedFile implements PicassoImageResource { + public static final int FEEDFILETYPE_FEEDIMAGE = 1; + + protected String title; + protected FeedComponent owner; + + public FeedImage(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/core/feed/FeedItem.java b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java new file mode 100644 index 000000000..55143b13b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -0,0 +1,332 @@ +package de.danoeh.antennapod.core.feed; + +import android.net.Uri; + +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.ShownotesProvider; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +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/core/feed/FeedMedia.java b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java new file mode 100644 index 000000000..ab87e822d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -0,0 +1,408 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.playback.Playable; + +public class FeedMedia extends FeedFile implements Playable { + private static final String TAG = "FeedMedia"; + + public static final int FEEDFILETYPE_FEEDMEDIA = 2; + public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; + + public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; + public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; + + private int duration; + private int position; // Current position in file + private int played_duration; // How many ms of this file have been played (for autoflattring) + private long size; // File size in Byte + private String mime_type; + private volatile FeedItem item; + private Date playbackCompletionDate; + + /* Used for loading item when restoring from parcel. */ + private long itemID; + + public FeedMedia(FeedItem i, String download_url, long size, + String mime_type) { + super(null, download_url, false); + this.item = i; + this.size = size; + this.mime_type = mime_type; + } + + public FeedMedia(long id, FeedItem item, int duration, int position, + long size, String mime_type, String file_url, String download_url, + boolean downloaded, Date playbackCompletionDate, int played_duration) { + super(file_url, download_url, downloaded); + this.id = id; + this.item = item; + this.duration = duration; + this.position = position; + this.played_duration = played_duration; + this.size = size; + this.mime_type = mime_type; + this.playbackCompletionDate = playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public FeedMedia(long id, FeedItem item) { + super(); + this.id = id; + this.item = item; + } + + @Override + public String getHumanReadableIdentifier() { + if (item != null && item.getTitle() != null) { + return item.getTitle(); + } else { + return download_url; + } + } + + /** + * Uses mimetype to determine the type of media. + */ + public MediaType getMediaType() { + if (mime_type == null || mime_type.isEmpty()) { + return MediaType.UNKNOWN; + } else { + if (mime_type.startsWith("audio")) { + return MediaType.AUDIO; + } else if (mime_type.startsWith("video")) { + return MediaType.VIDEO; + } else if (mime_type.equals("application/ogg")) { + return MediaType.AUDIO; + } + } + return MediaType.UNKNOWN; + } + + public void updateFromOther(FeedMedia other) { + super.updateFromOther(other); + if (other.size > 0) { + size = other.size; + } + if (other.mime_type != null) { + mime_type = other.mime_type; + } + } + + public boolean compareWithOther(FeedMedia other) { + if (super.compareWithOther(other)) { + return true; + } + if (other.mime_type != null) { + if (mime_type == null || !mime_type.equals(other.mime_type)) { + return true; + } + } + if (other.size > 0 && other.size != size) { + return true; + } + return false; + } + + /** + * Reads playback preferences to determine whether this FeedMedia object is + * currently being played. + */ + public boolean isPlaying() { + return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; + } + + @Override + public int getTypeAsInt() { + return FEEDFILETYPE_FEEDMEDIA; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + public int getPlayedDuration() { + return played_duration; + } + + public void setPlayedDuration(int played_duration) { + this.played_duration = played_duration; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getMime_type() { + return mime_type; + } + + public void setMime_type(String mime_type) { + this.mime_type = mime_type; + } + + public FeedItem getItem() { + return item; + } + + /** + * Sets the item object of this FeedMedia. If the given + * FeedItem object is not null, it's 'media'-attribute value + * will also be set to this media object. + */ + public void setItem(FeedItem item) { + this.item = item; + if (item != null && item.getMedia() != this) { + item.setMedia(this); + } + } + + public Date getPlaybackCompletionDate() { + return playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public void setPlaybackCompletionDate(Date playbackCompletionDate) { + this.playbackCompletionDate = playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public boolean isInProgress() { + return (this.position > 0); + } + + public FeedImage getImage() { + if (item != null) { + return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage(); + } + return null; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(id); + dest.writeLong(item.getId()); + + dest.writeInt(duration); + dest.writeInt(position); + dest.writeLong(size); + dest.writeString(mime_type); + dest.writeString(file_url); + dest.writeString(download_url); + dest.writeByte((byte) ((downloaded) ? 1 : 0)); + dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); + dest.writeInt(played_duration); + } + + @Override + public void writeToPreferences(Editor prefEditor) { + prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); + prefEditor.putLong(PREF_MEDIA_ID, id); + } + + @Override + public void loadMetadata() throws PlayableException { + if (item == null && itemID != 0) { + item = DBReader.getFeedItem(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/core/feed/FeedPreferences.java b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java new file mode 100644 index 000000000..2f0304182 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.Context; +import de.danoeh.antennapod.core.storage.DBWriter; +import org.apache.commons.lang3.StringUtils; + +/** + * Contains preferences for a single feed. + */ +public class FeedPreferences { + + private long feedID; + private boolean autoDownload; + private String username; + private String password; + + public FeedPreferences(long feedID, boolean autoDownload, String username, String password) { + this.feedID = feedID; + this.autoDownload = autoDownload; + this.username = username; + this.password = password; + } + + + /** + * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the + * comparison. + * + * @return True if the two objects are different. + */ + public boolean compareWithOther(FeedPreferences other) { + if (other == null) + return true; + if (!StringUtils.equals(username, other.username)) { + return true; + } + if (!StringUtils.equals(password, other.password)) { + return true; + } + return false; + } + + /** + * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded + * from the update. + */ + public void updateFromOther(FeedPreferences other) { + if (other == null) + return; + this.username = other.username; + this.password = other.password; + } + + public long getFeedID() { + return feedID; + } + + public void setFeedID(long feedID) { + this.feedID = feedID; + } + + public boolean getAutoDownload() { + return autoDownload; + } + + public void setAutoDownload(boolean autoDownload) { + this.autoDownload = autoDownload; + } + + public void save(Context context) { + DBWriter.setFeedPreferences(context, this); + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java b/app/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java new file mode 100644 index 000000000..f0ff03a93 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.feed; + +public class ID3Chapter extends Chapter { + public static final int CHAPTERTYPE_ID3CHAPTER = 2; + + /** + * Identifies the chapter in its ID3 tag. This attribute does not have to be + * store in the DB and is only used for parsing. + */ + private String id3ID; + + public ID3Chapter(String id3ID, long start) { + super(start); + this.id3ID = id3ID; + } + + public ID3Chapter(long start, String title, FeedItem item, String link) { + super(start, title, item, link); + } + + @Override + public String toString() { + return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start=" + + start + ", url=" + link + "]"; + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_ID3CHAPTER; + } + + public String getId3ID() { + return id3ID; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java b/app/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java new file mode 100644 index 000000000..7b3cb829d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java @@ -0,0 +1,5 @@ +package de.danoeh.antennapod.core.feed; + +public enum MediaType { + AUDIO, VIDEO, UNKNOWN +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java b/app/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java new file mode 100644 index 000000000..9aa8d3170 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.feed; + +public class SearchResult { + private FeedComponent component; + /** Additional information (e.g. where it was found) */ + private String subtitle; + /** Higher value means more importance */ + private int value; + + public SearchResult(FeedComponent component, int value, String subtitle) { + super(); + this.component = component; + this.value = value; + this.subtitle = subtitle; + } + + public FeedComponent getComponent() { + return component; + } + + public String getSubtitle() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle = subtitle; + } + + public int getValue() { + return value; + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java b/app/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java new file mode 100644 index 000000000..2dadd3ec8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.feed; + +public class SimpleChapter extends Chapter { + public static final int CHAPTERTYPE_SIMPLECHAPTER = 0; + + public SimpleChapter(long start, String title, FeedItem item, String link) { + super(start, title, item, link); + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_SIMPLECHAPTER; + } + + public void updateFromOther(SimpleChapter other) { + super.updateFromOther(other); + start = other.start; + if (other.title != null) { + title = other.title; + } + if (other.link != null) { + link = other.link; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java b/app/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java new file mode 100644 index 000000000..5b54a2d59 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java @@ -0,0 +1,109 @@ +package de.danoeh.antennapod.core.feed; + +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; + +import java.util.concurrent.TimeUnit; + +public class VorbisCommentChapter extends Chapter { + public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3; + + private static final int CHAPTERXXX_LENGTH = "chapterxxx".length(); + + private int vorbisCommentId; + + public VorbisCommentChapter(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + public VorbisCommentChapter(long start, String title, FeedItem item, + String link) { + super(start, title, item, link); + } + + @Override + public String toString() { + return "VorbisCommentChapter [id=" + id + ", title=" + title + + ", link=" + link + ", start=" + start + "]"; + } + + public static long getStartTimeFromValue(String value) + throws VorbisCommentReaderException { + String[] parts = value.split(":"); + if (parts.length >= 3) { + try { + long hours = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[0]), TimeUnit.HOURS); + long minutes = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[1]), TimeUnit.MINUTES); + if (parts[2].contains("-->")) { + parts[2] = parts[2].substring(0, parts[2].indexOf("-->")); + } + long seconds = TimeUnit.MILLISECONDS.convert( + ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS); + return hours + minutes + seconds; + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } else { + throw new VorbisCommentReaderException("Invalid time string"); + } + } + + /** + * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx* + * + * @return the id of the chapter key or -1 if the id couldn't be read. + * @throws VorbisCommentReaderException + * */ + public static int getIDFromKey(String key) + throws VorbisCommentReaderException { + if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx + try { + String strId = key.substring(8, 10); + return Integer.parseInt(strId); + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } + throw new VorbisCommentReaderException("key is too short (" + key + ")"); + } + + /** + * Get the string that comes after 'CHAPTERxxx', for example 'name' or + * 'url'. + */ + public static String getAttributeTypeFromKey(String key) { + if (key.length() > CHAPTERXXX_LENGTH) { + return key.substring(CHAPTERXXX_LENGTH, key.length()); + } + return null; + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_VORBISCOMMENT_CHAPTER; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setLink(String link) { + this.link = link; + } + + public void setStart(long start) { + this.start = start; + } + + public int getVorbisCommentId() { + return vorbisCommentId; + } + + public void setVorbisCommentId(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java new file mode 100644 index 000000000..117cbf96b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -0,0 +1,718 @@ +package de.danoeh.antennapod.core.gpoddernet; + +import org.apache.commons.lang3.Validate; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; + +/** + * Communicates with the gpodder.net service. + */ +public class GpodnetService { + + private static final String BASE_SCHEME = "https"; + + public static final String DEFAULT_BASE_HOST = "gpodder.net"; + private final String BASE_HOST; + + private final HttpClient httpClient; + + public GpodnetService() { + httpClient = AntennapodHttpClient.getHttpClient(); + BASE_HOST = GpodnetPreferences.getHostname(); + } + + /** + * Returns the [count] most used tags. + */ + public List 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.core.gpoddernet.model.GpodnetUploadChangesResponse} + * for details. + * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null. + * @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there + * is an authentication error. + */ + public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection 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/core/gpoddernet/GpodnetServiceAuthenticationException.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java new file mode 100644 index 000000000..8bd56218c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceAuthenticationException extends GpodnetServiceException { + + public GpodnetServiceAuthenticationException() { + super(); + } + + public GpodnetServiceAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public GpodnetServiceAuthenticationException(String message) { + super(message); + } + + public GpodnetServiceAuthenticationException(Throwable cause) { + super(cause); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java new file mode 100644 index 000000000..16f01f0f4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java @@ -0,0 +1,12 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException { + int statusCode; + + public GpodnetServiceBadStatusCodeException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java new file mode 100644 index 000000000..ce704f7e3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceException extends Exception { + + public GpodnetServiceException() { + } + + public GpodnetServiceException(String message) { + super(message); + } + + public GpodnetServiceException(Throwable cause) { + super(cause); + } + + public GpodnetServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java new file mode 100644 index 000000000..4885a243a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java @@ -0,0 +1,72 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +public class GpodnetDevice { + + private String id; + private String caption; + private DeviceType type; + private int subscriptions; + + public GpodnetDevice(String id, String caption, String type, + int subscriptions) { + Validate.notNull(id); + + this.id = id; + this.caption = caption; + this.type = DeviceType.fromString(type); + this.subscriptions = subscriptions; + } + + @Override + public String toString() { + return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type=" + + type + ", subscriptions=" + subscriptions + "]"; + } + + public static enum DeviceType { + DESKTOP, LAPTOP, MOBILE, SERVER, OTHER; + + static DeviceType fromString(String s) { + if (s == null) { + return OTHER; + } + + if (s.equals("desktop")) { + return DESKTOP; + } else if (s.equals("laptop")) { + return LAPTOP; + } else if (s.equals("mobile")) { + return MOBILE; + } else if (s.equals("server")) { + return SERVER; + } else { + return OTHER; + } + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + } + + public String getId() { + return id; + } + + public String getCaption() { + return caption; + } + + public DeviceType getType() { + return type; + } + + public int getSubscriptions() { + return subscriptions; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java new file mode 100644 index 000000000..afebf66ac --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java @@ -0,0 +1,65 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +public class GpodnetPodcast { + private String url; + private String title; + private String description; + private int subscribers; + private String logoUrl; + private String website; + private String mygpoLink; + + public GpodnetPodcast(String url, String title, String description, + int subscribers, String logoUrl, String website, String mygpoLink) { + Validate.notNull(url); + Validate.notNull(title); + Validate.notNull(description); + + this.url = url; + this.title = title; + this.description = description; + this.subscribers = subscribers; + this.logoUrl = logoUrl; + this.website = website; + this.mygpoLink = mygpoLink; + } + + @Override + public String toString() { + return "GpodnetPodcast [url=" + url + ", title=" + title + + ", description=" + description + ", subscribers=" + + subscribers + ", logoUrl=" + logoUrl + ", website=" + website + + ", mygpoLink=" + mygpoLink + "]"; + } + + public String getUrl() { + return url; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public int getSubscribers() { + return subscribers; + } + + public String getLogoUrl() { + return logoUrl; + } + + public String getWebsite() { + return website; + } + + public String getMygpoLink() { + return mygpoLink; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java new file mode 100644 index 000000000..a5cb8c0f0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + +public class GpodnetSubscriptionChange { + private List 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/core/gpoddernet/model/GpodnetTag.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java new file mode 100644 index 000000000..7178f4be5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +import java.util.Comparator; + +public class GpodnetTag { + + private String name; + private int usage; + + public GpodnetTag(String name, int usage) { + Validate.notNull(name); + + this.name = name; + this.usage = usage; + } + + public GpodnetTag(String name) { + super(); + this.name = name; + } + + @Override + public String toString() { + return "GpodnetTag [name=" + name + ", usage=" + usage + "]"; + } + + public String getName() { + return name; + } + + public int getUsage() { + return usage; + } + + public static class UsageComparator implements Comparator { + + @Override + public int compare(GpodnetTag o1, GpodnetTag o2) { + return o1.usage - o2.usage; + } + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java new file mode 100644 index 000000000..5a37efa5e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Object returned by {@link de.danoeh.antennapod.core.gpoddernet.GpodnetService} in uploadChanges method. + */ +public class GpodnetUploadChangesResponse { + + /** + * timestamp/ID that can be used for requesting changes since this upload. + */ + public final long timestamp; + + /** + * URLs that should be updated. The key of the map is the original URL, the value of the map + * is the sanitized URL. + */ + public final Map 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/core/opml/OpmlElement.java b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java new file mode 100644 index 000000000..8d0a4a842 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.opml; + +/** Represents a single feed in an OPML file. */ +public class OpmlElement { + private String text; + private String xmlUrl; + private String htmlUrl; + private String type; + + public OpmlElement() { + + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getXmlUrl() { + return xmlUrl; + } + + public void setXmlUrl(String xmlUrl) { + this.xmlUrl = xmlUrl; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public void setHtmlUrl(String htmlUrl) { + this.htmlUrl = htmlUrl; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java new file mode 100644 index 000000000..aa484954d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java @@ -0,0 +1,87 @@ +package de.danoeh.antennapod.core.opml; + +import android.util.Log; +import de.danoeh.antennapod.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/core/opml/OpmlSymbols.java b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java new file mode 100644 index 000000000..2b831ca2a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.opml; + +/** Contains symbols for reading and writing OPML documents. */ +public final class OpmlSymbols { + + public static final String OPML = "opml"; + public static final String BODY = "body"; + public static final String OUTLINE = "outline"; + public static final String TEXT = "text"; + public static final String XMLURL = "xmlUrl"; + public static final String HTMLURL = "htmlUrl"; + public static final String TYPE = "type"; + public static final String VERSION = "version"; + public static final String HEAD = "head"; + public static final String TITLE = "title"; + + private OpmlSymbols() { + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java new file mode 100644 index 000000000..fe14b4954 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java @@ -0,0 +1,65 @@ +package de.danoeh.antennapod.core.opml; + +import android.util.Log; +import android.util.Xml; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +/** Writes OPML documents. */ +public class OpmlWriter { + private static final String TAG = "OpmlWriter"; + private static final String ENCODING = "UTF-8"; + private static final String OPML_VERSION = "2.0"; + private static final String OPML_TITLE = "AntennaPod Subscriptions"; + + /** + * Takes a list of feeds and a writer and writes those into an OPML + * document. + * + * @throws IOException + * @throws IllegalStateException + * @throws IllegalArgumentException + */ + public void writeDocument(List 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/core/preferences/GpodnetPreferences.java b/app/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java new file mode 100644 index 000000000..716a74f53 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java @@ -0,0 +1,246 @@ +package de.danoeh.antennapod.core.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.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.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/core/preferences/PlaybackPreferences.java b/app/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java new file mode 100644 index 000000000..756b4067c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -0,0 +1,146 @@ +package de.danoeh.antennapod.core.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.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/core/preferences/UserPreferences.java b/app/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java new file mode 100644 index 000000000..1669fc601 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -0,0 +1,577 @@ +package de.danoeh.antennapod.core.preferences; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.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.core.receiver.FeedUpdateReceiver; + +/** + * Provides access to preferences set by the user in the settings screen. A + * private instance of this class must first be instantiated via + * createInstance() or otherwise every public method will throw an Exception + * when called. + */ +public class UserPreferences implements + SharedPreferences.OnSharedPreferenceChangeListener { + public static final String IMPORT_DIR = "import/"; + private static final String TAG = "UserPreferences"; + + public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; + public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; + public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; + public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; + public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; + public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; + public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; + public static final String PREF_THEME = "prefTheme"; + public static final String PREF_DATA_FOLDER = "prefDataFolder"; + public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; + public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter"; + private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; + public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; + private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; + private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; + public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; + + // 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, + 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/core/receiver/AlarmUpdateReceiver.java b/app/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java new file mode 100644 index 000000000..2057b0881 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java @@ -0,0 +1,33 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +/** Listens for events that make it necessary to reset the update alarm. */ +public class AlarmUpdateReceiver extends BroadcastReceiver { + private static final String TAG = "AlarmUpdateReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received intent"); + if (StringUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Resetting update alarm after reboot"); + } else if (StringUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Resetting update alarm after app upgrade"); + } + + UserPreferences.restartUpdateAlarm(UserPreferences.getUpdateInterval()); + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java b/app/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java new file mode 100644 index 000000000..e6b1a1b49 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.NetworkUtils; + +public class ConnectivityActionReceiver extends BroadcastReceiver { + private static final String TAG = "ConnectivityActionReceiver"; + + @Override + public void onReceive(final Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received intent"); + + if (NetworkUtils.autodownloadNetworkAvailable(context)) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "auto-dl network available, starting auto-download"); + DBTasks.autodownloadUndownloadedItems(context); + } else { // if new network is Wi-Fi, finish ongoing downloads, + // otherwise cancel all downloads + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getActiveNetworkInfo(); + if (ni == null || ni.getType() != ConnectivityManager.TYPE_WIFI) { + if (BuildConfig.DEBUG) + Log.i(TAG, + "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads"); + DownloadRequester.getInstance().cancelAllDownloads(context); + } + + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/app/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java new file mode 100644 index 000000000..ec63bc2ae --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBTasks; + +/** Refreshes all feeds when it receives an intent */ +public class FeedUpdateReceiver extends BroadcastReceiver { + private static final String TAG = "FeedUpdateReceiver"; + public static final String ACTION_REFRESH_FEEDS = "de.danoeh.antennapod.feedupdatereceiver.refreshFeeds"; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_REFRESH_FEEDS)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received intent"); + boolean mobileUpdate = UserPreferences.isAllowMobileUpdate(); + if (mobileUpdate || connectedToWifi(context)) { + DBTasks.refreshExpiredFeeds(context); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Blocking automatic update: no wifi available / no mobile updates allowed"); + } + } + } + + private boolean connectedToWifi(Context context) { + ConnectivityManager connManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWifi = connManager + .getNetworkInfo(ConnectivityManager.TYPE_WIFI); + + return mWifi.isConnected(); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/app/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java new file mode 100644 index 000000000..be54148cf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.service.playback.PlaybackService; + +/** Receives media button events. */ +public class MediaButtonReceiver extends BroadcastReceiver { + private static final String TAG = "MediaButtonReceiver"; + public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE"; + + public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"; + + @Override + public void onReceive(Context context, Intent intent) { + if (BuildConfig.DEBUG) Log.d(TAG, "Received intent"); + KeyEvent event = (KeyEvent) intent.getExtras().get( + Intent.EXTRA_KEY_EVENT); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + Intent serviceIntent = new Intent(context, PlaybackService.class); + int keycode = event.getKeyCode(); + serviceIntent.putExtra(EXTRA_KEYCODE, keycode); + context.startService(serviceIntent); + } + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java b/app/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java new file mode 100644 index 000000000..3dcfecdbd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java @@ -0,0 +1,50 @@ +package de.danoeh.antennapod.core.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.core.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/core/service/GpodnetSyncService.java b/app/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java new file mode 100644 index 000000000..8a2659029 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java @@ -0,0 +1,245 @@ +package de.danoeh.antennapod.core.service; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.NetworkUtils; + +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); +// TODO getGpodnetSyncServiceErrorNotificationPendingIntent + 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/core/service/download/APRedirectHandler.java b/app/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java new file mode 100644 index 000000000..1c62eaa77 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.core.service.download; + +import android.util.Log; +import de.danoeh.antennapod.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/core/service/download/DownloadService.java b/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java new file mode 100644 index 000000000..e9381d509 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -0,0 +1,1230 @@ +package de.danoeh.antennapod.core.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.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.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.fragment.DownloadsFragment; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.InvalidFeedException; + +/** + * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. + * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of + * the intent. + * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the + * type of the feedfile. + */ +public class DownloadService extends Service { + private static final String TAG = "DownloadService"; + + /** + * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the + * object whose download should be cancelled. + */ + public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.core.service.cancelDownload"; + + /** + * Cancels all running downloads. + */ + public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAllDownloads"; + + /** + * Extra for ACTION_CANCEL_DOWNLOAD + */ + public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; + + /** + * Sent by the DownloadService when the content of the downloads list + * changes. + */ + public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.core.service.downloadsContentChanged"; + + /** + * Extra for ACTION_ENQUEUE_DOWNLOAD intent. + */ + public static final String EXTRA_REQUEST = "request"; + + /** + * Stores new media files that will be queued for auto-download if possible. + */ + private List 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() { // TODO getNotificationContentIntent + 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) { // TODO getReportNotificationContentIntent + 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(); + + // TODO getAuthentificationNotificationContentIntent + 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(); + DownloadRequester requester = DownloadRequester.getInstance(); + int tasks = 0; + + try { + DownloadRequest request = completedRequests.take(); + parserService.submit(new FeedParserTask(request)); + tasks++; + } catch (InterruptedException e) { + return null; + } + + tasks += pollCompletedDownloads(); + + isCollectingRequests = true; + + if (requester.isDownloadingFeeds()) { + // wait for completion of more downloads + long startTime = System.currentTimeMillis(); + long currentTime = startTime; + while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { + try { + if (BuildConfig.DEBUG) + Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); + sleep(startTime + WAIT_TIMEOUT - currentTime); + } catch (InterruptedException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "interrupted while waiting for more downloads"); + tasks += pollCompletedDownloads(); + } finally { + currentTime = System.currentTimeMillis(); + } + } + + tasks += pollCompletedDownloads(); + + } + + isCollectingRequests = false; + + for (int i = 0; i < tasks; i++) { + try { + Feed f = parserService.take().get(); + if (f != null) { + results.add(f); + } + } catch (InterruptedException e) { + e.printStackTrace(); + + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + return results; + } + + private int pollCompletedDownloads() { + int tasks = 0; + for (int i = 0; i < completedRequests.size(); i++) { + parserService.submit(new FeedParserTask(completedRequests.poll())); + tasks++; + } + return tasks; + } + + @Override + public void run() { + while (isActive) { + final List feeds = collectCompletedRequests(); + + if (feeds == null) { + continue; + } + + if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + feeds.size() + " feeds"); + + for (Feed feed : feeds) { + removeDuplicateImages(feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. + } + + // Save information of feed in DB + if (dbUpdateFuture != null) { + try { + dbUpdateFuture.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + dbUpdateFuture = dbService.submit(new Runnable() { + @Override + public void run() { + Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, feeds.toArray(new Feed[feeds.size()])); + + for (Feed savedFeed : savedFeeds) { + // Download Feed Image if provided and not downloaded + if (savedFeed.getImage() != null + && savedFeed.getImage().isDownloaded() == false) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Feed has image; Downloading...."); + savedFeed.getImage().setOwner(savedFeed); + final Feed savedFeedRef = savedFeed; + try { + requester.downloadImage(DownloadService.this, + savedFeedRef.getImage()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + DownloadService.this, + new DownloadStatus( + savedFeedRef.getImage(), + savedFeedRef + .getImage() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage() + ) + ); + } + } + + // queue new media files for automatic download + for (FeedItem item : savedFeed.getItems()) { + if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { + newMediaFiles.add(item.getMedia().getId()); + } + } + + numberOfDownloads.decrementAndGet(); + } + + sendDownloadHandledIntent(); + + queryDownloadsAsync(); + } + }); + + } + + if (dbUpdateFuture != null) { + try { + dbUpdateFuture.get(); + } catch (InterruptedException e) { + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down"); + + } + + private class FeedParserTask implements Callable { + + private DownloadRequest request; + + private FeedParserTask(DownloadRequest request) { + this.request = request; + } + + @Override + public Feed call() throws Exception { + return parseFeed(request); + } + } + + private Feed parseFeed(DownloadRequest request) { + Feed savedFeed = null; + + Feed feed = new Feed(request.getSource(), new Date()); + feed.setFile_url(request.getDestination()); + feed.setId(request.getFeedfileId()); + feed.setDownloaded(true); + feed.setPreferences(new FeedPreferences(0, true, request.getUsername(), request.getPassword())); + + DownloadError reason = null; + String reasonDetailed = null; + boolean successful = true; + FeedHandler feedHandler = new FeedHandler(); + + try { + feed = feedHandler.parseFeed(feed).feed; + if (BuildConfig.DEBUG) + Log.d(TAG, feed.getTitle() + " parsed"); + if (checkFeedData(feed) == false) { + throw new InvalidFeedException(); + } + + } catch (SAXException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (IOException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (ParserConfigurationException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (UnsupportedFeedtypeException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_UNSUPPORTED_TYPE; + reasonDetailed = e.getMessage(); + } catch (InvalidFeedException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } + + // cleanup(); + if (savedFeed == null) { + savedFeed = feed; + } + + + if (successful) { + return savedFeed; + } else { + numberOfDownloads.decrementAndGet(); + saveDownloadStatus(new DownloadStatus(savedFeed, + savedFeed.getHumanReadableIdentifier(), reason, successful, + reasonDetailed)); + return null; + } + } + + + /** + * Checks if the feed was parsed correctly. + */ + private boolean checkFeedData(Feed feed) { + if (feed.getTitle() == null) { + Log.e(TAG, "Feed has no title."); + return false; + } + if (!hasValidFeedItems(feed)) { + Log.e(TAG, "Feed has invalid items"); + return false; + } + return true; + } + + /** + * Checks if the FeedItems of this feed have images that point + * to the same URL. If two FeedItems have an image that points to + * the same URL, the reference of the second item is removed, so that every image + * reference is unique. + */ + private void removeDuplicateImages(Feed feed) { + for (int x = 0; x < feed.getItems().size(); x++) { + for (int y = x + 1; y < feed.getItems().size(); y++) { + FeedItem item1 = feed.getItems().get(x); + FeedItem item2 = feed.getItems().get(y); + if (item1.hasItemImage() && item2.hasItemImage()) { + if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) { + item2.setImage(null); + } + } + } + } + } + + private boolean hasValidFeedItems(Feed feed) { + for (FeedItem item : feed.getItems()) { + if (item.getTitle() == null) { + Log.e(TAG, "Item has no title"); + return false; + } + if (item.getPubDate() == null) { + Log.e(TAG, + "Item has no pubDate. Using current time as pubDate"); + if (item.getTitle() != null) { + Log.e(TAG, "Title of invalid item: " + item.getTitle()); + } + item.setPubDate(new Date()); + } + } + return true; + } + + /** + * Delete files that aren't needed anymore + */ + private void cleanup(Feed feed) { + if (feed.getFile_url() != null) { + if (new File(feed.getFile_url()).delete()) + if (BuildConfig.DEBUG) + Log.d(TAG, "Successfully deleted cache file."); + else + Log.e(TAG, "Failed to delete cache file."); + feed.setFile_url(null); + } else if (BuildConfig.DEBUG) { + Log.d(TAG, "Didn't delete cache file: File url is not set."); + } + } + + public void shutdown() { + isActive = false; + if (isCollectingRequests) { + interrupt(); + } + } + + public void submitCompletedDownload(DownloadRequest request) { + completedRequests.offer(request); + if (isCollectingRequests) { + interrupt(); + } + } + + } + + /** + * Handles failed downloads. + *

+ * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location + * of the downloaded file. + *

+ * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. + */ + class FailedDownloadHandler implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + FailedDownloadHandler(DownloadStatus status, DownloadRequest request) { + this.request = request; + this.status = status; + } + + @Override + public void run() { + if (request.isDeleteOnFailure()) { + if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); + } else { + File dest = new File(request.getDestination()); + if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + Log.d(TAG, "File has been partially downloaded. Writing file url"); + FeedMedia media = DBReader.getFeedMedia(DownloadService.this, request.getFeedfileId()); + media.setFile_url(request.getDestination()); + try { + DBWriter.setFeedMedia(DownloadService.this, media).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + } + } + } + + /** + * Handles a completed image download. + */ + class ImageHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { + Validate.notNull(status); + Validate.notNull(request); + + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId()); + if (image == null) { + throw new IllegalStateException("Could not find downloaded image in database"); + } + + image.setFile_url(request.getDestination()); + image.setDownloaded(true); + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + DBWriter.setFeedImage(DownloadService.this, image); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Handles a completed media download. + */ + class MediaHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { + Validate.notNull(status); + Validate.notNull(request); + + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedMedia media = DBReader.getFeedMedia(DownloadService.this, + request.getFeedfileId()); + if (media == null) { + throw new IllegalStateException( + "Could not find downloaded media object in database"); + } + boolean chaptersRead = false; + media.setDownloaded(true); + media.setFile_url(request.getDestination()); + + // Get duration + MediaMetadataRetriever mmr = null; + try { + mmr = new MediaMetadataRetriever(); + mmr.setDataSource(media.getFile_url()); + String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + media.setDuration(Integer.parseInt(durationStr)); + if (BuildConfig.DEBUG) + Log.d(TAG, "Duration of file is " + media.getDuration()); + } catch (NumberFormatException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } finally { + if (mmr != null) { + mmr.release(); + } + } + + if (media.getItem().getChapters() == null) { + ChapterUtils.loadChaptersFromFileUrl(media); + if (media.getItem().getChapters() != null) { + chaptersRead = true; + } + } + + try { + if (chaptersRead) { + DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); + } + DBWriter.setFeedMedia(DownloadService.this, media).get(); + if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { + DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get(); + } + } catch (ExecutionException e) { + e.printStackTrace(); + status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); + } + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Schedules the notification updater task if it hasn't been scheduled yet. + */ + private void setupNotificationUpdater() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting up notification updater"); + if (notificationUpdater == null) { + notificationUpdater = new NotificationUpdater(); + notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( + notificationUpdater, 5L, 5L, TimeUnit.SECONDS); + } + } + + private void cancelNotificationUpdater() { + boolean result = false; + if (notificationUpdaterFuture != null) { + result = notificationUpdaterFuture.cancel(true); + } + notificationUpdater = null; + notificationUpdaterFuture = null; + Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); + } + + private class NotificationUpdater implements Runnable { + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + Notification n = updateNotifications(); + if (n != null) { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, n); + } + } + }); + } + } + + public List getDownloads() { + return downloads; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java b/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java new file mode 100644 index 000000000..d05650d10 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java @@ -0,0 +1,181 @@ +package de.danoeh.antennapod.core.service.download; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.util.DownloadError; + +import java.util.Date; + +/** Contains status attributes for one download */ +public class DownloadStatus { + /** + * Downloaders should use this constant for the size attribute if necessary + * so that the listadapters etc. can react properly. + */ + public static final int SIZE_UNKNOWN = -1; + + // ----------------------------------- ATTRIBUTES STORED IN DB + /** Unique id for storing the object in database. */ + protected long id; + /** + * A human-readable string which is shown to the user so that he can + * identify the download. Should be the title of the item/feed/media or the + * URL if the download has no other title. + */ + protected String title; + protected DownloadError reason; + /** + * A message which can be presented to the user to give more information. + * Should be null if Download was successful. + */ + protected String reasonDetailed; + protected boolean successful; + protected Date completionDate; + protected long feedfileId; + /** + * Is used to determine the type of the feedfile even if the feedfile does + * not exist anymore. The value should be FEEDFILETYPE_FEED, + * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA + */ + protected int feedfileType; + + // ------------------------------------ NOT STORED IN DB + protected boolean done; + protected boolean cancelled; + + /** Constructor for restoring Download status entries from DB. */ + public DownloadStatus(long id, String title, long feedfileId, + int feedfileType, boolean successful, DownloadError reason, + Date completionDate, String reasonDetailed) { + this.id = id; + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.reason = reason; + this.successful = successful; + this.completionDate = (Date) completionDate.clone(); + this.reasonDetailed = reasonDetailed; + this.feedfileType = feedfileType; + } + + public DownloadStatus(DownloadRequest request, DownloadError reason, + boolean successful, boolean cancelled, String reasonDetailed) { + Validate.notNull(request); + + this.title = request.getTitle(); + this.feedfileId = request.getFeedfileId(); + this.feedfileType = request.getFeedfileType(); + this.reason = reason; + this.successful = successful; + this.cancelled = cancelled; + this.reasonDetailed = reasonDetailed; + this.completionDate = new Date(); + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, + boolean successful, String reasonDetailed) { + Validate.notNull(feedfile); + + this.title = title; + this.done = true; + this.feedfileId = feedfile.getId(); + this.feedfileType = feedfile.getTypeAsInt(); + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(long feedfileId, int feedfileType, String title, + DownloadError reason, boolean successful, String reasonDetailed) { + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + @Override + public String toString() { + return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" + + reason + ", reasonDetailed=" + reasonDetailed + + ", successful=" + successful + ", completionDate=" + + completionDate + ", feedfileId=" + feedfileId + + ", feedfileType=" + feedfileType + ", done=" + done + + ", cancelled=" + cancelled + "]"; + } + + public long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public DownloadError getReason() { + return reason; + } + + public String getReasonDetailed() { + return reasonDetailed; + } + + public boolean isSuccessful() { + return successful; + } + + public Date getCompletionDate() { + return (Date) completionDate.clone(); + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public boolean isDone() { + return done; + } + + public boolean isCancelled() { + return cancelled; + } + + public void setSuccessful() { + this.successful = true; + this.reason = DownloadError.SUCCESS; + this.done = true; + } + + public void setFailed(DownloadError reason, String reasonDetailed) { + this.successful = false; + this.reason = reason; + this.reasonDetailed = reasonDetailed; + this.done = true; + } + + public void setCancelled() { + this.successful = false; + this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; + this.done = true; + this.cancelled = true; + } + + public void setCompletionDate(Date completionDate) { + this.completionDate = (Date) completionDate.clone(); + } + + public void setId(long id) { + this.id = id; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/app/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java new file mode 100644 index 000000000..5af9c2d05 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.service.download; + +import android.content.Context; +import android.net.wifi.WifiManager; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.R; + +import java.util.concurrent.Callable; + +/** Downloads files */ +public abstract class Downloader implements Callable { + private static final String TAG = "Downloader"; + + protected volatile boolean finished; + + protected volatile boolean cancelled; + + protected DownloadRequest request; + protected DownloadStatus result; + + public Downloader(DownloadRequest request) { + super(); + this.request = request; + this.request.setStatusMsg(R.string.download_pending); + this.cancelled = false; + this.result = new DownloadStatus(request, null, false, false, null); + } + + protected abstract void download(); + + public final Downloader call() { + WifiManager wifiManager = (WifiManager) PodcastApp.getInstance().getSystemService(Context.WIFI_SERVICE); + WifiManager.WifiLock wifiLock = null; + if (wifiManager != null) { + wifiLock = wifiManager.createWifiLock(TAG); + wifiLock.acquire(); + } + + download(); + + if (wifiLock != null) { + wifiLock.release(); + } + + if (result == null) { + throw new IllegalStateException( + "Downloader hasn't created DownloadStatus object"); + } + finished = true; + return this; + } + + public DownloadRequest getDownloadRequest() { + return request; + } + + public DownloadStatus getResult() { + return result; + } + + public boolean isFinished() { + return finished; + } + + public void cancel() { + cancelled = true; + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java b/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java new file mode 100644 index 000000000..2d9347b0a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java @@ -0,0 +1,10 @@ +package de.danoeh.antennapod.core.service.download; + +/** + * Callback used by the Downloader-classes to notify the requester that the + * download has completed. + */ +public interface DownloaderCallback { + + public void onDownloadCompleted(Downloader downloader); +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/app/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java new file mode 100644 index 000000000..cba59be01 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -0,0 +1,246 @@ +package de.danoeh.antennapod.core.service.download; + +import android.net.http.AndroidHttpClient; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.URIUtil; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.message.BasicHeader; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +public class HttpDownloader extends Downloader { + private static final String TAG = "HttpDownloader"; + + private static final int BUFFER_SIZE = 8 * 1024; + + public HttpDownloader(DownloadRequest request) { + super(request); + } + + @Override + protected void download() { + File destination = new File(request.getDestination()); + final boolean fileExists = destination.exists(); + + if (request.isDeleteOnFailure() && fileExists) { + Log.w(TAG, "File already exists"); + if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { + onFail(DownloadError.ERROR_FILE_EXISTS, null); + return; + } else { + onSuccess(); + return; + } + } + + HttpClient httpClient = AntennapodHttpClient.getHttpClient(); + RandomAccessFile out = null; + InputStream connection = null; + try { + HttpGet httpGet = new HttpGet(URIUtil.getURIFromRequestUrl(request.getSource())); + + // add authentication information + String userInfo = httpGet.getURI().getUserInfo(); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + httpGet.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(parts[0], parts[1]), + "UTF-8", false)); + } + } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + httpGet.addHeader(BasicScheme.authenticate(new UsernamePasswordCredentials(request.getUsername(), + request.getPassword()), "UTF-8", false)); + } + + // add range header if necessary + if (fileExists) { + request.setSoFar(destination.length()); + httpGet.addHeader(new BasicHeader("Range", + "bytes=" + request.getSoFar() + "-")); + if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar()); + } + + HttpResponse response = httpClient.execute(httpGet); + HttpEntity httpEntity = response.getEntity(); + int responseCode = response.getStatusLine().getStatusCode(); + Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); + + final boolean isGzip = contentEncodingHeader != null && + contentEncodingHeader.getValue().equalsIgnoreCase("gzip"); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Response code is " + responseCode); + + if (responseCode / 100 != 2 || httpEntity == null) { + final DownloadError error; + final String details; + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + error = DownloadError.ERROR_UNAUTHORIZED; + details = String.valueOf(responseCode); + } else { + error = DownloadError.ERROR_HTTP_DATA_ERROR; + details = String.valueOf(responseCode); + } + onFail(error, details); + return; + } + + if (!StorageUtils.storageAvailable(PodcastApp.getInstance())) { + onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); + return; + } + + connection = new BufferedInputStream(AndroidHttpClient + .getUngzippedContent(httpEntity)); + + Header[] contentRangeHeaders = (fileExists) ? response.getHeaders("Content-Range") : null; + + if (fileExists && responseCode == HttpStatus.SC_PARTIAL_CONTENT + && contentRangeHeaders != null && contentRangeHeaders.length > 0) { + String start = contentRangeHeaders[0].getValue().substring("bytes ".length(), + contentRangeHeaders[0].getValue().indexOf("-")); + request.setSoFar(Long.valueOf(start)); + Log.d(TAG, "Starting download at position " + request.getSoFar()); + + out = new RandomAccessFile(destination, "rw"); + out.seek(request.getSoFar()); + } else { + destination.delete(); + destination.createNewFile(); + out = new RandomAccessFile(destination, "rw"); + } + + + byte[] buffer = new byte[BUFFER_SIZE]; + int count = 0; + request.setStatusMsg(R.string.download_running); + if (BuildConfig.DEBUG) + Log.d(TAG, "Getting size of download"); + request.setSize(httpEntity.getContentLength() + request.getSoFar()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Size is " + request.getSize()); + if (request.getSize() < 0) { + request.setSize(DownloadStatus.SIZE_UNKNOWN); + } + + long freeSpace = StorageUtils.getFreeSpaceAvailable(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Free space is " + freeSpace); + + if (request.getSize() != DownloadStatus.SIZE_UNKNOWN + && request.getSize() > freeSpace) { + onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); + return; + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting download"); + while (!cancelled + && (count = connection.read(buffer)) != -1) { + out.write(buffer, 0, count); + request.setSoFar(request.getSoFar() + count); + request.setProgressPercent((int) (((double) request + .getSoFar() / (double) request + .getSize()) * 100)); + } + if (cancelled) { + onCancelled(); + } else { + // check if size specified in the response header is the same as the size of the + // written file. This check cannot be made if compression was used + if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && + request.getSoFar() != request.getSize()) { + onFail(DownloadError.ERROR_IO_ERROR, + "Download completed but size: " + + request.getSoFar() + + " does not equal expected size " + + request.getSize() + ); + return; + } + onSuccess(); + } + + } catch (IllegalArgumentException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); + } catch (SocketTimeoutException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); + } catch (UnknownHostException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); + } catch (NullPointerException e) { + // might be thrown by connection.getInputStream() + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); + } finally { + IOUtils.closeQuietly(out); + AntennapodHttpClient.cleanup(); + } + } + + private void onSuccess() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Download was successful"); + result.setSuccessful(); + } + + private void onFail(DownloadError reason, String reasonDetailed) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Download failed"); + } + result.setFailed(reason, reasonDetailed); + if (request.isDeleteOnFailure()) { + cleanup(); + } + } + + private void onCancelled() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Download was cancelled"); + result.setCancelled(); + cleanup(); + } + + /** + * Deletes unfinished downloads. + */ + private void cleanup() { + if (request.getDestination() != null) { + File dest = new File(request.getDestination()); + if (dest.exists()) { + boolean rc = dest.delete(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + + rc); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "cleanup() didn't delete file: does not exist."); + } + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java new file mode 100644 index 000000000..c191c9521 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -0,0 +1,1080 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.media.RemoteControlClient.MetadataEditor; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.widget.Toast; + +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.List; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.AudioplayerActivity; +import de.danoeh.antennapod.activity.VideoplayerActivity; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.receiver.PlayerWidget; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** + * Controls the MediaPlayer that plays a FeedMedia-file + */ +public class PlaybackService extends Service { + /** + * Logging tag + */ + private static final String TAG = "PlaybackService"; + + /** + * Parcelable of type Playable. + */ + public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; + /** + * True if media should be streamed. + */ + public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream"; + /** + * True if playback should be started immediately after media has been + * prepared. + */ + public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared"; + + public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; + + public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged"; + private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; + private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; + + public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.core.service.playerNotification"; + public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode"; + public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType"; + + /** + * If the PlaybackService receives this action, it will stop playback and + * try to shutdown. + */ + public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService"; + + /** + * If the PlaybackService receives this action, it will end playback of the + * current episode and load the next episode if there is one available. + */ + public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode"; + + /** + * Used in NOTIFICATION_TYPE_RELOAD. + */ + public static final int EXTRA_CODE_AUDIO = 1; + public static final int EXTRA_CODE_VIDEO = 2; + + public static final int NOTIFICATION_TYPE_ERROR = 0; + public static final int NOTIFICATION_TYPE_INFO = 1; + public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; + + /** + * Receivers of this intent should update their information about the curently playing media + */ + public static final int NOTIFICATION_TYPE_RELOAD = 3; + /** + * The state of the sleeptimer changed. + */ + public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; + public static final int NOTIFICATION_TYPE_BUFFER_START = 5; + public static final int NOTIFICATION_TYPE_BUFFER_END = 6; + /** + * No more episodes are going to be played. + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; + + /** + * Playback speed has changed + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; + + /** + * Returned by getPositionSafe() or getDurationSafe() if the playbackService + * is in an invalid state. + */ + public static final int INVALID_TIME = -1; + + /** + * Is true if service is running. + */ + public static boolean isRunning = false; + /** + * Is true if service has received a valid start command. + */ + public static boolean started = false; + + private static final int NOTIFICATION_ID = 1; + + private RemoteControlClient remoteControlClient; + private PlaybackServiceMediaPlayer mediaPlayer; + private PlaybackServiceTaskManager taskManager; + + private static volatile MediaType currentMediaType = MediaType.UNKNOWN; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public PlaybackService getService() { + return PlaybackService.this; + } + } + + @Override + public boolean onUnbind(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received onUnbind event"); + return super.onUnbind(intent); + } + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. If the playbackservice is not + * running, the type of the last played media will be looked up. + */ + public static Intent getPlayerActivityIntent(Context context) { + if (isRunning) { // TODO getPlayerActivityIntent + if (currentMediaType == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } else { + if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + } + + /** + * Same as getPlayerActivityIntent(context), but here the type of activity + * depends on the FeedMedia that is provided as an argument. + */ + public static Intent getPlayerActivityIntent(Context context, Playable media) { + MediaType mt = media.getMediaType(); + if (mt == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service created."); + isRunning = true; + + registerReceiver(headsetDisconnected, new IntentFilter( + Intent.ACTION_HEADSET_PLUG)); + registerReceiver(shutdownReceiver, new IntentFilter( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + registerReceiver(audioBecomingNoisy, new IntentFilter( + AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( + ACTION_SKIP_CURRENT_EPISODE)); + remoteControlClient = setupRemoteControlClient(); + taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); + mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); + + } + + @SuppressLint("NewApi") + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + isRunning = false; + started = false; + currentMediaType = MediaType.UNKNOWN; + + unregisterReceiver(headsetDisconnected); + unregisterReceiver(shutdownReceiver); + unregisterReceiver(audioBecomingNoisy); + unregisterReceiver(skipCurrentEpisodeReceiver); + mediaPlayer.shutdown(); + taskManager.shutdown(); + } + + @Override + public IBinder onBind(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received onBind event"); + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + if (BuildConfig.DEBUG) + Log.d(TAG, "OnStartCommand called"); + final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); + if (keycode == -1 && playable == null) { + Log.e(TAG, "PlaybackService was started with no arguments"); + stopSelf(); + } + + if ((flags & Service.START_FLAG_REDELIVERY) != 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + stopForeground(true); + } else { + + if (keycode != -1) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received media button event"); + handleKeycode(keycode); + } else { + started = true; + boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, + true); + boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); + boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); + } + } + + return Service.START_REDELIVER_INTENT; + } + + /** + * Handles media button events + */ + private void handleKeycode(int keycode) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling keycode: " + keycode); + + final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + final PlayerStatus status = info.playerStatus; + switch (keycode) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if (status == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, true); + } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.PREPARING) { + mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + if (status == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, true); + } + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs()); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs()); + break; + default: + if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something + String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + break; + } + } + + /** + * Called by a mediaplayer Activity as soon as it has prepared its + * mediaplayer. + */ + public void setVideoSurface(SurfaceHolder sh) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting display"); + mediaPlayer.setVideoSurface(sh); + } + + /** + * Called when the surface holder of the mediaplayer has to be changed. + */ + private void resetVideoSurface() { + taskManager.cancelPositionSaver(); + mediaPlayer.resetVideoSurface(); + } + + public void notifyVideoSurfaceAbandoned() { + stopForeground(true); + mediaPlayer.resetVideoSurface(); + } + + private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); + } + + @Override + public void onSleepTimerExpired() { + mediaPlayer.pause(true, true); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + + @Override + public void onWidgetUpdaterTick() { + updateWidget(); + } + + @Override + public void onChapterLoaded(Playable media) { + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + } + }; + + private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + currentMediaType = mediaPlayer.getCurrentMediaType(); + switch (newInfo.playerStatus) { + case INITIALIZED: + writePlaybackPreferences(); + break; + + case PREPARED: + taskManager.startChapterLoader(newInfo.playable); + break; + + case PAUSED: + taskManager.cancelPositionSaver(); + saveCurrentPosition(false, 0); + taskManager.cancelWidgetUpdater(); + stopForeground(true); + break; + + case STOPPED: + //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); + //stopSelf(); + break; + + case PLAYING: + if (BuildConfig.DEBUG) + Log.d(TAG, "Audiofocus successfully requested"); + if (BuildConfig.DEBUG) + Log.d(TAG, "Resuming/Starting playback"); + + taskManager.startPositionSaver(); + taskManager.startWidgetUpdater(); + setupNotification(newInfo); + break; + case ERROR: + writePlaybackPreferencesNoMediaPlaying(); + break; + + } + + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + updateWidget(); + refreshRemoteControlClientState(newInfo); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); + } + + @Override + public void shouldStop() { + stopSelf(); + } + + @Override + public void playbackSpeedChanged(float s) { + sendNotificationBroadcast( + NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); + } + + @Override + public void onBufferingUpdate(int percent) { + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); + } + + @Override + public boolean onMediaPlayerInfo(int code) { + switch (code) { + case MediaPlayer.MEDIA_INFO_BUFFERING_START: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); + return true; + case MediaPlayer.MEDIA_INFO_BUFFERING_END: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); + return true; + default: + return false; + } + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + final String TAG = "PlaybackService.onErrorListener"; + Log.w(TAG, "An error has occured: " + what + " " + extra); + if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, false); + } + sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); + writePlaybackPreferencesNoMediaPlaying(); + stopSelf(); + return true; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + PlaybackService.this.endPlayback(true); + return true; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return remoteControlClient; + } + }; + + private void endPlayback(boolean playNextEpisode) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Playback ended"); + + final Playable media = mediaPlayer.getPSMPInfo().playable; + if (media == null) { + Log.e(TAG, "Cannot end playback: media was null"); + return; + } + + taskManager.cancelPositionSaver(); + + boolean isInQueue = false; + FeedItem nextItem = null; + + if (media instanceof FeedMedia) { + FeedItem item = ((FeedMedia) media).getItem(); + DBWriter.markItemRead(PlaybackService.this, item, true, true); + + try { + final List queue = taskManager.getQueue(); + isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); + nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); + } catch (InterruptedException e) { + e.printStackTrace(); + // isInQueue remains false + } + if (isInQueue) { + DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); + } + DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + + // auto-flattr if enabled + if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { + DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); + } + } + + // Load next episode if previous episode was in the queue and if there + // is an episode in the queue left. + // Start playback immediately if continuous playback is enabled + Playable nextMedia = null; + boolean loadNextItem = isInQueue && nextItem != null; + playNextEpisode = playNextEpisode && loadNextItem + && UserPreferences.isFollowQueue(); + if (loadNextItem) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading next item in queue"); + nextMedia = nextItem.getMedia(); + } + final boolean prepareImmediately; + final boolean startWhenPrepared; + final boolean stream; + + if (playNextEpisode) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Playback of next episode will start immediately."); + prepareImmediately = startWhenPrepared = true; + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No more episodes available to play"); + + prepareImmediately = startWhenPrepared = false; + stopForeground(true); + stopWidgetUpdater(); + } + + writePlaybackPreferencesNoMediaPlaying(); + if (nextMedia != null) { + stream = !media.localFileAvailable(); + mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); + } else { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + mediaPlayer.stop(); + //stopSelf(); + } + } + + public void setSleepTimer(long waitingTime) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + taskManager.setSleepTimer(waitingTime); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + public void disableSleepTimer() { + taskManager.disableSleepTimer(); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + private void writePlaybackPreferencesNoMediaPlaying() { + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.commit(); + } + + + private void writePlaybackPreferences() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Writing playback preferences"); + + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + MediaType mediaType = mediaPlayer.getCurrentMediaType(); + boolean stream = mediaPlayer.isStreaming(); + + if (info.playable != null) { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + info.playable.getPlayableType()); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, + stream); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, + mediaType == MediaType.VIDEO); + if (info.playable instanceof FeedMedia) { + FeedMedia fMedia = (FeedMedia) info.playable; + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + fMedia.getItem().getFeed().getId()); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + fMedia.getId()); + } else { + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + info.playable.writeToPreferences(editor); + } else { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + + editor.commit(); + } + + /** + * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. + */ + private void postStatusUpdateIntent() { + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + } + + private void sendNotificationBroadcast(int type, int code) { + Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); + intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); + intent.putExtra(EXTRA_NOTIFICATION_CODE, code); + sendBroadcast(intent); + } + + /** + * Used by setupNotification to load notification data in another thread. + */ + private AsyncTask notificationSetupTask; + + /** + * Prepares notification and starts the service in the foreground. + */ + @SuppressLint("NewApi") + private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { + final PendingIntent pIntent = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT); + + if (notificationSetupTask != null) { + notificationSetupTask.cancel(true); + } + notificationSetupTask = new AsyncTask() { + Bitmap icon = null; + + @Override + protected Void doInBackground(Void... params) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting background work"); + if (android.os.Build.VERSION.SDK_INT >= 11) { + if (info.playable != null) { + try { + int iconSize = getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + icon = PicassoProvider.getMediaMetadataPicassoInstance(PlaybackService.this) + .load(info.playable.getImageUri()) + .resize(iconSize, iconSize) + .get(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + if (icon == null) { + icon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_stat_antenna); + } + + return null; + } + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING + && info.playable != null) { + String contentText = info.playable.getFeedTitle(); + String contentTitle = info.playable.getEpisodeTitle(); + Notification notification = null; + if (android.os.Build.VERSION.SDK_INT >= 16) { + Intent pauseButtonIntent = new Intent( + PlaybackService.this, PlaybackService.class); + pauseButtonIntent.putExtra( + MediaButtonReceiver.EXTRA_KEYCODE, + KeyEvent.KEYCODE_MEDIA_PAUSE); + PendingIntent pauseButtonPendingIntent = PendingIntent + .getService(PlaybackService.this, 0, + pauseButtonIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + Notification.Builder notificationBuilder = new Notification.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setOngoing(true) + .setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_stat_antenna) + .addAction(android.R.drawable.ic_media_pause, + getString(R.string.pause_label), + pauseButtonPendingIntent); + notification = notificationBuilder.build(); + } else { + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText).setOngoing(true) + .setContentIntent(pIntent).setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_stat_antenna); + notification = notificationBuilder.getNotification(); + } + startForeground(NOTIFICATION_ID, notification); + if (BuildConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + } + + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + notificationSetupTask + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + notificationSetupTask.execute(); + } + + } + + /** + * Saves the current position of the media file to the DB + * + * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects + * @param deltaPlayedDuration value by which played_duration should be increased. + */ + private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { + int position = getCurrentPosition(); + int duration = getDuration(); + float playbackSpeed = getCurrentPlaybackSpeed(); + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Saving current position to " + position); + if (updatePlayedDuration && playable instanceof FeedMedia) { + FeedMedia m = (FeedMedia) playable; + FeedItem item = m.getItem(); + m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); + // Auto flattr + if (isAutoFlattrable(m) && + (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { + + if (BuildConfig.DEBUG) + Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) + + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); + DBTasks.flattrItemIfLoggedIn(this, item); + } + } + playable.saveCurrentPosition(PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()), + position + ); + } + } + + private void stopWidgetUpdater() { + taskManager.cancelWidgetUpdater(); + sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE)); + } + + private void updateWidget() { + PlaybackService.this.sendBroadcast(new Intent( + PlayerWidget.FORCE_WIDGET_UPDATE)); + } + + public boolean sleepTimerActive() { + return taskManager.isSleepTimerActive(); + } + + public long getSleepTimerTimeLeft() { + return taskManager.getSleepTimerTimeLeft(); + } + + @SuppressLint("NewApi") + private RemoteControlClient setupRemoteControlClient() { + if (Build.VERSION.SDK_INT < 14) { + return null; + } + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(new ComponentName(getPackageName(), + MediaButtonReceiver.class.getName())); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( + getApplicationContext(), 0, mediaButtonIntent, 0); + remoteControlClient = new RemoteControlClient(mediaPendingIntent); + int controlFlags; + if (android.os.Build.VERSION.SDK_INT < 16) { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } else { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + remoteControlClient.setTransportControlFlags(controlFlags); + return remoteControlClient; + } + + /** + * Refresh player status and metadata. + */ + @SuppressLint("NewApi") + private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) { + if (android.os.Build.VERSION.SDK_INT >= 14) { + if (remoteControlClient != null) { + switch (info.playerStatus) { + case PLAYING: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + break; + case PAUSED: + case INITIALIZED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + break; + case STOPPED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + break; + case ERROR: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); + break; + default: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); + } + if (info.playable != null) { + MetadataEditor editor = remoteControlClient + .editMetadata(false); + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, + info.playable.getEpisodeTitle()); + + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, + info.playable.getFeedTitle()); + + editor.apply(); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "RemoteControlClient state was refreshed"); + } + } + } + + private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { + boolean isPlaying = false; + + if (info.playerStatus == PlayerStatus.PLAYING) { + isPlaying = true; + } + + if (info.playable != null) { + Intent i = new Intent(whatChanged); + i.putExtra("id", 1); + i.putExtra("artist", ""); + i.putExtra("album", info.playable.getFeedTitle()); + i.putExtra("track", info.playable.getEpisodeTitle()); + i.putExtra("playing", isPlaying); + final List queue = taskManager.getQueueIfLoaded(); + if (queue != null) { + i.putExtra("ListSize", queue.size()); + } + i.putExtra("duration", info.playable.getDuration()); + i.putExtra("position", info.playable.getPosition()); + sendBroadcast(i); + } + } + + /** + * Pauses playback when the headset is disconnected and the preference is + * set + */ + private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { + private static final String TAG = "headsetDisconnected"; + private static final int UNPLUGGED = 0; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { + int state = intent.getIntExtra("state", -1); + if (state != -1) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Headset plug event. State is " + state); + if (state == UNPLUGGED) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Headset was unplugged during playback."); + pauseIfPauseOnDisconnect(); + } + } else { + Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); + } + } + } + }; + + private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + // sound is about to change, eg. bluetooth -> speaker + if (BuildConfig.DEBUG) + Log.d(TAG, "Pausing playback because audio is becoming noisy"); + pauseIfPauseOnDisconnect(); + } + // android.media.AUDIO_BECOMING_NOISY + }; + + /** + * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. + */ + private void pauseIfPauseOnDisconnect() { + if (UserPreferences.isPauseOnHeadsetDisconnect()) { + mediaPlayer.pause(true, true); + } + } + + private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + stopSelf(); + } + } + + }; + + private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); + mediaPlayer.endPlayback(); + } + } + }; + + public static MediaType getCurrentMediaType() { + return currentMediaType; + } + + public void resume() { + mediaPlayer.resume(); + } + + public void prepare() { + mediaPlayer.prepare(); + } + + public void pause(boolean abandonAudioFocus, boolean reinit) { + mediaPlayer.pause(abandonAudioFocus, reinit); + } + + public void reinit() { + mediaPlayer.reinit(); + } + + public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { + return mediaPlayer.getPSMPInfo(); + } + + public PlayerStatus getStatus() { + return mediaPlayer.getPSMPInfo().playerStatus; + } + + public Playable getPlayable() { + return mediaPlayer.getPSMPInfo().playable; + } + + public void setSpeed(float speed) { + mediaPlayer.setSpeed(speed); + } + + public boolean canSetSpeed() { + return mediaPlayer.canSetSpeed(); + } + + public float getCurrentPlaybackSpeed() { + return mediaPlayer.getPlaybackSpeed(); + } + + public boolean isStartWhenPrepared() { + return mediaPlayer.isStartWhenPrepared(); + } + + public void setStartWhenPrepared(boolean s) { + mediaPlayer.setStartWhenPrepared(s); + } + + + public void seekTo(final int t) { + mediaPlayer.seekTo(t); + } + + + public void seekDelta(final int d) { + mediaPlayer.seekDelta(d); + } + + /** + * @see de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.core.feed.Chapter) + */ + public void seekToChapter(Chapter c) { + mediaPlayer.seekToChapter(c); + } + + /** + * call getDuration() on mediaplayer or return INVALID_TIME if player is in + * an invalid state. + */ + public int getDuration() { + return mediaPlayer.getDuration(); + } + + /** + * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player + * is in an invalid state. + */ + public int getCurrentPosition() { + return mediaPlayer.getPosition(); + } + + public boolean isStreaming() { + return mediaPlayer.isStreaming(); + } + + public Pair getVideoSize() { + return mediaPlayer.getVideoSize(); + } + + private boolean isAutoFlattrable(Playable p) { + if (p != null && p instanceof FeedMedia) { + FeedMedia media = (FeedMedia) p; + FeedItem item = ((FeedMedia) p).getItem(); + return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); + } else { + return false; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java new file mode 100644 index 000000000..62ad59166 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -0,0 +1,979 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.net.wifi.WifiManager; +import android.os.PowerManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.util.Pair; +import android.view.SurfaceHolder; + +import org.apache.commons.lang3.Validate; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.util.playback.AudioPlayer; +import de.danoeh.antennapod.core.util.playback.IPlayer; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.VideoPlayer; + +/** + * Manages the MediaPlayer object of the PlaybackService. + */ +public class PlaybackServiceMediaPlayer { + public static final String TAG = "PlaybackServiceMediaPlayer"; + + /** + * Return value of some PSMP methods if the method call failed. + */ + public static final int INVALID_TIME = -1; + + private final AudioManager audioManager; + + private volatile PlayerStatus playerStatus; + private volatile PlayerStatus statusBeforeSeeking; + private volatile IPlayer mediaPlayer; + private volatile Playable media; + + private volatile boolean stream; + private volatile MediaType mediaType; + private volatile AtomicBoolean startWhenPrepared; + private volatile boolean pausedBecauseOfTransientAudiofocusLoss; + private volatile Pair videoSize; + + /** + * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads + * have to wait until these operations have finished. + */ + private final ReentrantLock playerLock; + + private final PSMPCallback callback; + private final Context context; + + private final ThreadPoolExecutor executor; + + /** + * A wifi-lock that is acquired if the media file is being streamed. + */ + private WifiManager.WifiLock wifiLock; + + public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { + Validate.notNull(context); + Validate.notNull(callback); + + this.context = context; + this.callback = callback; + this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + this.playerLock = new ReentrantLock(); + this.startWhenPrepared = new AtomicBoolean(false); + executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque(), + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); + } + } + ); + + mediaPlayer = null; + statusBeforeSeeking = null; + pausedBecauseOfTransientAudiofocusLoss = false; + mediaType = MediaType.UNKNOWN; + playerStatus = PlayerStatus.STOPPED; + videoSize = null; + } + + /** + * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing + * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will + * not do anything. + * Whether playback starts immediately depends on the given parameters. See below for more details. + *

+ * States: + * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. + *

+ * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If + * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. + *

+ * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object + * will enter the ERROR state. + *

+ * This method is executed on an internal executor service. + * + * @param playable The Playable object that is supposed to be played. This parameter must not be null. + * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via + * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by + * the Android MediaPlayer via getStreamUrl. + * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the + * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared + * for playback immediately (see 'prepareImmediately' parameter for more details) + * @param prepareImmediately Set to true if the method should also prepare the episode for playback. + */ + public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { + Validate.notNull(playable); + + if (BuildConfig.DEBUG) Log.d(TAG, "Play media object."); + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + try { + playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); + } catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } finally { + playerLock.unlock(); + } + } + }); + } + + /** + * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if + * the given playable parameter is the same object as the currently playing media. + *

+ * This method requires the playerLock and is executed on the caller's thread. + * + * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean) + */ + private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { + Validate.notNull(playable); + if (!playerLock.isHeldByCurrentThread()) + throw new IllegalStateException("method requires playerLock"); + + + if (media != null) { + if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { + // episode is already playing -> ignore method call + if (BuildConfig.DEBUG) + Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); + return; + } else { + // stop playback of this episode + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { + mediaPlayer.stop(); + } + setPlayerStatus(PlayerStatus.INDETERMINATE, null); + } + } + + this.media = playable; + this.stream = stream; + this.mediaType = media.getMediaType(); + this.videoSize = null; + createMediaPlayer(); + PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared); + setPlayerStatus(PlayerStatus.INITIALIZING, media); + try { + media.loadMetadata(); + if (stream) { + mediaPlayer.setDataSource(media.getStreamUrl()); + } else { + mediaPlayer.setDataSource(media.getLocalMediaUrl()); + } + setPlayerStatus(PlayerStatus.INITIALIZED, media); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT); + } + + if (prepareImmediately) { + setPlayerStatus(PlayerStatus.PREPARING, media); + mediaPlayer.prepare(); + onPrepared(startWhenPrepared); + } + + } catch (Playable.PlayableException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IllegalStateException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + + + /** + * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. + * nothing will happen. + *

+ * This method is executed on an internal executor service. + */ + public void resume() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + resumeSync(); + playerLock.unlock(); + } + }); + } + + private void resumeSync() { + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { + int focusGained = audioManager.requestAudioFocus( + audioFocusChangeListener, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + acquireWifiLockIfNecessary(); + setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); + mediaPlayer.start(); + if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + setPlayerStatus(PlayerStatus.PLAYING, media); + pausedBecauseOfTransientAudiofocusLoss = false; + if (android.os.Build.VERSION.SDK_INT >= 14) { + RemoteControlClient remoteControlClient = callback.getRemoteControlClient(); + if (remoteControlClient != null) { + audioManager + .registerRemoteControlClient(remoteControlClient); + } + } + audioManager + .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), + MediaButtonReceiver.class.getName())); + media.onPlaybackStart(); + + } else { + if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); + } + } + + + /** + * Saves the current position and pauses playback. Note that, if audiofocus + * is abandoned, the lockscreen controls will also disapear. + *

+ * This method is executed on an internal executor service. + * + * @param abandonFocus is true if the service should release audio focus + * @param reinit is true if service should reinit after pausing if the media + * file is being streamed + */ + public void pause(final boolean abandonFocus, final boolean reinit) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + if (playerStatus == PlayerStatus.PLAYING) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Pausing playback."); + mediaPlayer.pause(); + setPlayerStatus(PlayerStatus.PAUSED, media); + + if (abandonFocus) { + audioManager.abandonAudioFocus(audioFocusChangeListener); + pausedBecauseOfTransientAudiofocusLoss = false; + } + if (stream && reinit) { + reinit(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Prepared media player for playback if the service is in the INITALIZED + * state. + *

+ * This method is executed on an internal executor service. + */ + public void prepare() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus == PlayerStatus.INITIALIZED) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Preparing media player"); + setPlayerStatus(PlayerStatus.PREPARING, media); + try { + mediaPlayer.prepare(); + onPrepared(startWhenPrepared.get()); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + playerLock.unlock(); + + } + }); + } + + /** + * Called after media player has been prepared. This method is executed on the caller's thread. + */ + void onPrepared(final boolean startWhenPrepared) { + playerLock.lock(); + + if (playerStatus != PlayerStatus.PREPARING) { + playerLock.unlock(); + throw new IllegalStateException("Player is not in PREPARING state"); + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Resource prepared"); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight()); + } + + if (media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + if (media.getDuration() == 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting duration of media"); + media.setDuration(mediaPlayer.getDuration()); + } + setPlayerStatus(PlayerStatus.PREPARED, media); + + if (startWhenPrepared) { + resumeSync(); + } + + playerLock.unlock(); + } + + /** + * Resets the media player and moves it into INITIALIZED state. + *

+ * This method is executed on an internal executor service. + */ + public void reinit() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + if (media != null) { + playMediaObject(media, true, stream, startWhenPrepared.get(), false); + } else if (mediaPlayer != null) { + mediaPlayer.reset(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + } + playerLock.unlock(); + } + }); + } + + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * + * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0 + *

+ * This method is executed on the caller's thread. + */ + private void seekToSync(int t) { + if (t < 0) { + t = 0; + } + playerLock.lock(); + + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + if (stream) { + // statusBeforeSeeking = playerStatus; + // setPlayerStatus(PlayerStatus.SEEKING, media); + } + mediaPlayer.seekTo(t); + + } else if (playerStatus == PlayerStatus.INITIALIZED) { + media.setPosition(t); + startWhenPrepared.set(true); + prepare(); + } + playerLock.unlock(); + } + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * Invalid time values (< 0) will be ignored. + *

+ * This method is executed on an internal executor service. + */ + public void seekTo(final int t) { + executor.submit(new Runnable() { + @Override + public void run() { + seekToSync(t); + } + }); + } + + /** + * Seek a specific position from the current position + * + * @param d offset from current position (positive or negative) + */ + public void seekDelta(final int d) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + int currentPosition = getPosition(); + if (currentPosition != INVALID_TIME) { + seekToSync(currentPosition + d); + } else { + Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Seek to the start of the specified chapter. + */ + public void seekToChapter(Chapter c) { + Validate.notNull(c); + + seekTo((int) c.getStart()); + } + + /** + * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. + */ + public int getDuration() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getDuration(); + } else if (media != null && media.getDuration() > 0) { + retVal = media.getDuration(); + } + + playerLock.unlock(); + return retVal; + } + + /** + * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. + */ + public int getPosition() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getCurrentPosition(); + } else if (media != null && media.getPosition() > 0) { + retVal = media.getPosition(); + } + + playerLock.unlock(); + return retVal; + } + + public boolean isStartWhenPrepared() { + return startWhenPrepared.get(); + } + + public void setStartWhenPrepared(boolean startWhenPrepared) { + this.startWhenPrepared.set(startWhenPrepared); + } + + /** + * Returns true if the playback speed can be adjusted. + */ + public boolean canSetSpeed() { + boolean retVal = false; + if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { + retVal = (mediaPlayer).canSetSpeed(); + } + return retVal; + } + + /** + * Sets the playback speed. + * This method is executed on the caller's thread. + */ + private void setSpeedSync(float speed) { + playerLock.lock(); + if (media != null && media.getMediaType() == MediaType.AUDIO) { + if (mediaPlayer.canSetSpeed()) { + mediaPlayer.setPlaybackSpeed((float) speed); + if (BuildConfig.DEBUG) + Log.d(TAG, "Playback speed was set to " + speed); + callback.playbackSpeedChanged(speed); + } + } + playerLock.unlock(); + } + + /** + * Sets the playback speed. + * This method is executed on an internal executor service. + */ + public void setSpeed(final float speed) { + executor.submit(new Runnable() { + @Override + public void run() { + setSpeedSync(speed); + } + }); + } + + /** + * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. + */ + public float getPlaybackSpeed() { + if (!playerLock.tryLock()) { + return 1; + } + + float retVal = 1; + if ((playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) { + retVal = mediaPlayer.getCurrentSpeedMultiplier(); + } + playerLock.unlock(); + return retVal; + } + + public MediaType getCurrentMediaType() { + return mediaType; + } + + public boolean isStreaming() { + return stream; + } + + + /** + * Releases internally used resources. This method should only be called when the object is not used anymore. + */ + public void shutdown() { + executor.shutdown(); + if (mediaPlayer != null) { + mediaPlayer.release(); + } + releaseWifiLockIfNecessary(); + } + + public void setVideoSurface(final SurfaceHolder surface) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (mediaPlayer != null) { + mediaPlayer.setDisplay(surface); + } + playerLock.unlock(); + } + }); + } + + public void resetVideoSurface() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Resetting video surface"); + mediaPlayer.setDisplay(null); + reinit(); + playerLock.unlock(); + } + }); + } + + /** + * Return width and height of the currently playing video as a pair. + * + * @return Width and height as a Pair or null if the video size could not be determined. The method might still + * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return + * invalid values. + */ + public Pair getVideoSize() { + if (!playerLock.tryLock()) { + // use cached value if lock can't be aquired + return videoSize; + } + Pair res; + if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { + res = null; + } else { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight()); + res = videoSize; + } + playerLock.unlock(); + return res; + } + + /** + * Returns a PSMInfo object that contains information about the current state of the PSMP object. + * + * @return The PSMPInfo object. + */ + public synchronized PSMPInfo getPSMPInfo() { + return new PSMPInfo(playerStatus, media); + } + + /** + * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time + * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). + *

+ * This method will notify the callback about the change of the player status (even if the new status is the same + * as the old one). + * + * @param newStatus The new PlayerStatus. This must not be null. + * @param newMedia The new playable object of the PSMP object. This can be null. + */ + private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { + Validate.notNull(newStatus); + + if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); + + this.playerStatus = newStatus; + this.media = newMedia; + callback.statusChanged(new PSMPInfo(playerStatus, media)); + } + + private IPlayer createMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.release(); + } + if (media == null || media.getMediaType() == MediaType.VIDEO) { + mediaPlayer = new VideoPlayer(); + } else { + mediaPlayer = new AudioPlayer(context); + } + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + return setMediaPlayerListeners(mediaPlayer); + } + + private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { + + @Override + public void onAudioFocusChange(final int focusChange) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + // If there is an incoming call, playback should be paused permanently + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final int callState = (tm != null) ? tm.getCallState() : 0; + if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState); + Log.i(TAG, "Call state:" + callState); + + if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus"); + pause(true, false); + callback.shouldStop(); + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Gained audio focus"); + if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now + resume(); + } else { // we ducked => raise audio level back + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_RAISE, 0); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + if (playerStatus == PlayerStatus.PLAYING) { + if (!UserPreferences.shouldPauseForFocusLoss()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, 0); + pausedBecauseOfTransientAudiofocusLoss = false; + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + if (playerStatus == PlayerStatus.PLAYING) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + playerLock.unlock(); + } + } + }); + } + }; + + + public void endPlayback() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + + if (playerStatus != PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.INDETERMINATE, media); + } + if (mediaPlayer != null) { + mediaPlayer.reset(); + + } + audioManager.abandonAudioFocus(audioFocusChangeListener); + callback.endPlayback(true); + + playerLock.unlock(); + } + }); + } + + /** + * Moves the PlaybackServiceMediaPlayer into STOPPED state. This call is only valid if the player is currently in + * INDETERMINATE state, for example after a call to endPlayback. + * This method will only take care of changing the PlayerStatus of this object! Other tasks like + * abandoning audio focus have to be done with other methods. + */ + public void stop() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + + if (playerStatus == PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.STOPPED, null); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); + } + playerLock.unlock(); + + } + }); + } + + private synchronized void acquireWifiLockIfNecessary() { + if (stream) { + if (wifiLock == null) { + wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) + .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); + wifiLock.setReferenceCounted(false); + } + wifiLock.acquire(); + } + } + + private synchronized void releaseWifiLockIfNecessary() { + if (wifiLock != null && wifiLock.isHeld()) { + wifiLock.release(); + } + } + + /** + * Holds information about a PSMP object. + */ + public class PSMPInfo { + public PlayerStatus playerStatus; + public Playable playable; + + public PSMPInfo(PlayerStatus playerStatus, Playable playable) { + this.playerStatus = playerStatus; + this.playable = playable; + } + } + + public static interface PSMPCallback { + public void statusChanged(PSMPInfo newInfo); + + public void shouldStop(); + + public void playbackSpeedChanged(float s); + + public void onBufferingUpdate(int percent); + + public boolean onMediaPlayerInfo(int code); + + public boolean onMediaPlayerError(Object inObj, int what, int extra); + + public boolean endPlayback(boolean playNextEpisode); + + public RemoteControlClient getRemoteControlClient(); + } + + private IPlayer setMediaPlayerListeners(IPlayer mp) { + if (mp != null && media != null) { + if (media.getMediaType() == MediaType.AUDIO) { + ((AudioPlayer) mp) + .setOnCompletionListener(audioCompletionListener); + ((AudioPlayer) mp) + .setOnSeekCompleteListener(audioSeekCompleteListener); + ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); + ((AudioPlayer) mp) + .setOnBufferingUpdateListener(audioBufferingUpdateListener); + ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); + } else { + ((VideoPlayer) mp) + .setOnCompletionListener(videoCompletionListener); + ((VideoPlayer) mp) + .setOnSeekCompleteListener(videoSeekCompleteListener); + ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); + ((VideoPlayer) mp) + .setOnBufferingUpdateListener(videoBufferingUpdateListener); + ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); + } + } + return mp; + } + + private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(com.aocate.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(android.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private void genericOnCompletion() { + endPlayback(); + } + + private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, + int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private void genericOnBufferingUpdate(int percent) { + callback.onBufferingUpdate(percent); + } + + private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericInfoListener(what); + } + }; + + private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { + return genericInfoListener(what); + } + }; + + private boolean genericInfoListener(int what) { + return callback.onMediaPlayerInfo(what); + } + + private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericOnError(mp, what, extra); + } + }; + + private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(android.media.MediaPlayer mp, int what, int extra) { + return genericOnError(mp, what, extra); + } + }; + + private boolean genericOnError(Object inObj, int what, int extra) { + return callback.onMediaPlayerError(inObj, what, extra); + } + + private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(com.aocate.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(android.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final void genericSeekCompleteListener() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (playerStatus == PlayerStatus.SEEKING) { + setPlayerStatus(statusBeforeSeeking, media); + } + playerLock.unlock(); + } + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java new file mode 100644 index 000000000..1b33e8667 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -0,0 +1,384 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.Context; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.playback.Playable; + +import java.util.List; +import java.util.concurrent.*; + +/** + * Manages the background tasks of PlaybackSerivce, i.e. + * the sleep timer, the position saver, the widget updater and + * the queue loader. + *

+ * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) + * to notify the PlaybackService about updates from the running tasks. + */ +public class PlaybackServiceTaskManager { + private static final String TAG = "PlaybackServiceTaskManager"; + + /** + * Update interval of position saver in milliseconds. + */ + public static final int POSITION_SAVER_WAITING_INTERVAL = 5000; + /** + * Notification interval of widget updater in milliseconds. + */ + public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500; + + private static final int SCHED_EX_POOL_SIZE = 2; + private final ScheduledThreadPoolExecutor schedExecutor; + + private ScheduledFuture positionSaverFuture; + private ScheduledFuture widgetUpdaterFuture; + private ScheduledFuture sleepTimerFuture; + private volatile Future> queueFuture; + private volatile Future chapterLoaderFuture; + + private SleepTimer sleepTimer; + + private final Context context; + private final PSTMCallback callback; + + /** + * Sets up a new PSTM. This method will also start the queue loader task. + * + * @param context + * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. + */ + public PlaybackServiceTaskManager(Context context, PSTMCallback callback) { + Validate.notNull(context); + Validate.notNull(callback); + + this.context = context; + this.callback = callback; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + loadQueue(); + EventDistributor.getInstance().register(eventDistributorListener); + } + + private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { + cancelQueueLoader(); + loadQueue(); + } + } + }; + + private synchronized boolean isQueueLoaderActive() { + return queueFuture != null && !queueFuture.isDone(); + } + + private synchronized void cancelQueueLoader() { + if (isQueueLoaderActive()) { + queueFuture.cancel(true); + } + } + + private synchronized void loadQueue() { + if (!isQueueLoaderActive()) { + queueFuture = schedExecutor.submit(new Callable>() { + @Override + public List call() throws Exception { + return DBReader.getQueue(context); + } + }); + } + } + + /** + * Returns the queue if it is already loaded or null if it hasn't been loaded yet. + * In order to wait until the queue has been loaded, use getQueue() + */ + public synchronized List getQueueIfLoaded() { + if (queueFuture.isDone()) { + try { + return queueFuture.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + return null; + } + + /** + * Returns the queue or waits until the PSTM has loaded the queue from the database. + */ + public synchronized List getQueue() throws InterruptedException { + try { + return queueFuture.get(); + } catch (ExecutionException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Starts the position saver task. If the position saver is already active, nothing will happen. + */ + public synchronized void startPositionSaver() { + if (!isPositionSaverActive()) { + Runnable positionSaver = new Runnable() { + @Override + public void run() { + callback.positionSaverTick(); + } + }; + positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, + POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); + + if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); + } else { + if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); + } + } + + /** + * Returns true if the position saver is currently running. + */ + public synchronized boolean isPositionSaverActive() { + return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone(); + } + + /** + * Cancels the position saver. If the position saver is not running, nothing will happen. + */ + public synchronized void cancelPositionSaver() { + if (isPositionSaverActive()) { + positionSaverFuture.cancel(false); + if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver"); + } + } + + /** + * Starts the widget updater task. If the widget updater is already active, nothing will happen. + */ + public synchronized void startWidgetUpdater() { + if (!isWidgetUpdaterActive()) { + Runnable widgetUpdater = new Runnable() { + @Override + public void run() { + callback.onWidgetUpdaterTick(); + } + }; + widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, + WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); + + if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater"); + } else { + if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored."); + } + } + + /** + * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be + * cancelled first. + * After waitingTime has elapsed, onSleepTimerExpired() will be called. + * + * @throws java.lang.IllegalArgumentException if waitingTime <= 0 + */ + public synchronized void setSleepTimer(long waitingTime) { + Validate.isTrue(waitingTime > 0, "Waiting time <= 0"); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + if (isSleepTimerActive()) { + sleepTimerFuture.cancel(true); + } + sleepTimer = new SleepTimer(waitingTime); + sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); + } + + /** + * Returns true if the sleep timer is currently active. + */ + public synchronized boolean isSleepTimerActive() { + return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting; + } + + /** + * Disables the sleep timer. If the sleep timer is not active, nothing will happen. + */ + public synchronized void disableSleepTimer() { + if (isSleepTimerActive()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Disabling sleep timer"); + sleepTimerFuture.cancel(true); + } + } + + /** + * Returns the current sleep timer time or 0 if the sleep timer is not active. + */ + public synchronized long getSleepTimerTimeLeft() { + if (isSleepTimerActive()) { + return sleepTimer.getWaitingTime(); + } else { + return 0; + } + } + + + /** + * Returns true if the widget updater is currently running. + */ + public synchronized boolean isWidgetUpdaterActive() { + return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone(); + } + + /** + * Cancels the widget updater. If the widget updater is not running, nothing will happen. + */ + public synchronized void cancelWidgetUpdater() { + if (isWidgetUpdaterActive()) { + widgetUpdaterFuture.cancel(false); + if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater"); + } + } + + private synchronized void cancelChapterLoader() { + if (isChapterLoaderActive()) { + chapterLoaderFuture.cancel(true); + } + } + + private synchronized boolean isChapterLoaderActive() { + return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); + } + + /** + * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, + * it will be cancelled first. + * On completion, the callback's onChapterLoaded method will be called. + */ + public synchronized void startChapterLoader(final Playable media) { + Validate.notNull(media); + + if (isChapterLoaderActive()) { + cancelChapterLoader(); + } + + Runnable chapterLoader = new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Chapter loader started"); + if (media.getChapters() == null) { + media.loadChapterMarks(); + if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) { + callback.onChapterLoaded(media); + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Chapter loader stopped"); + } + }; + chapterLoaderFuture = schedExecutor.submit(chapterLoader); + } + + + /** + * Cancels all tasks. The PSTM will be in the initial state after execution of this method. + */ + public synchronized void cancelAllTasks() { + cancelPositionSaver(); + cancelWidgetUpdater(); + disableSleepTimer(); + cancelQueueLoader(); + cancelChapterLoader(); + } + + /** + * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after + * execution of this method. + */ + public synchronized void shutdown() { + EventDistributor.getInstance().unregister(eventDistributorListener); + cancelAllTasks(); + schedExecutor.shutdown(); + } + + /** + * Sleeps for a given time and then pauses playback. + */ + private class SleepTimer implements Runnable { + private static final String TAG = "SleepTimer"; + private static final long UPDATE_INTERVALL = 1000L; + private volatile long waitingTime; + private volatile boolean isWaiting; + + public SleepTimer(long waitingTime) { + super(); + this.waitingTime = waitingTime; + isWaiting = true; + } + + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting"); + while (waitingTime > 0) { + try { + Thread.sleep(UPDATE_INTERVALL); + waitingTime -= UPDATE_INTERVALL; + + if (waitingTime <= 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Waiting completed"); + postExecute(); + if (!Thread.currentThread().isInterrupted()) { + callback.onSleepTimerExpired(); + } + + } + } catch (InterruptedException e) { + Log.d(TAG, "Thread was interrupted while waiting"); + break; + } + } + postExecute(); + } + + protected void postExecute() { + isWaiting = false; + } + + public long getWaitingTime() { + return waitingTime; + } + + public boolean isWaiting() { + return isWaiting; + } + + } + + public static interface PSTMCallback { + void positionSaverTick(); + + void onSleepTimerExpired(); + + void onWidgetUpdaterTick(); + + void onChapterLoaded(Playable media); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java new file mode 100644 index 000000000..1ad0c25d9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.core.service.playback; + +public enum PlayerStatus { + INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state. + ERROR, + PREPARING, + PAUSED, + PLAYING, + STOPPED, + PREPARED, + SEEKING, + INITIALIZING, // playback service is loading the Playable's metadata + INITIALIZED // playback service was started, data source of media player was set. +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerWidgetService.java b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerWidgetService.java new file mode 100644 index 000000000..495e2c0f2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerWidgetService.java @@ -0,0 +1,190 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.widget.RemoteViews; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.receiver.PlayerWidget; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** Updates the state of the player widget */ +public class PlayerWidgetService extends Service { + private static final String TAG = "PlayerWidgetService"; + + private PlaybackService playbackService; + /** True while service is updating the widget */ + private volatile boolean isUpdating; + + public PlayerWidgetService() { + } + + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service created"); + isUpdating = false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + try { + unbindService(mConnection); + } catch (IllegalArgumentException e) { + Log.w(TAG, "IllegalArgumentException when trying to unbind service"); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (!isUpdating) { + if (playbackService == null && PlaybackService.isRunning) { + bindService(new Intent(this, PlaybackService.class), + mConnection, 0); + } else { + startViewUpdaterIfNotRunning(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Service was called while updating. Ignoring update request"); + } + return Service.START_NOT_STICKY; + } + + private void updateViews() { + if (playbackService == null) { + return; + } + isUpdating = true; + + ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(this); + RemoteViews views = new RemoteViews(getPackageName(), + R.layout.player_widget); + PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), 0); + + views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer); + final Playable media = playbackService.getPlayable(); + if (playbackService != null && media != null) { + PlayerStatus status = playbackService.getStatus(); + + views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); + + if (status == PlayerStatus.PLAYING) { + String progressString = getProgressString(playbackService); + if (progressString != null) { + views.setTextViewText(R.id.txtvProgress, progressString); + } + views.setImageViewResource(R.id.butPlay, R.drawable.av_pause_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); + } + } else { + views.setImageViewResource(R.id.butPlay, R.drawable.av_play_dark); + if (Build.VERSION.SDK_INT >= 15) { + views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); + } + } + views.setOnClickPendingIntent(R.id.butPlay, + createMediaButtonIntent()); + } else { + views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE); + views.setTextViewText(R.id.txtvTitle, + this.getString(R.string.no_media_playing_label)); + views.setImageViewResource(R.id.butPlay, R.drawable.av_play); + + } + + manager.updateAppWidget(playerWidget, views); + isUpdating = false; + } + + /** Creates an intent which fakes a mediabutton press */ + private PendingIntent createMediaButtonIntent() { + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); + Intent startingIntent = new Intent( + MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); + startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + + return PendingIntent.getBroadcast(this, 0, startingIntent, 0); + } + + private String getProgressString(PlaybackService ps) { + int position = ps.getCurrentPosition(); + int duration = ps.getDuration(); + if (position != PlaybackService.INVALID_TIME + && duration != PlaybackService.INVALID_TIME) { + return Converter.getDurationStringLong(position) + " / " + + Converter.getDurationStringLong(duration); + } else { + return null; + } + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Connection to service established"); + playbackService = ((PlaybackService.LocalBinder) service) + .getService(); + startViewUpdaterIfNotRunning(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + playbackService = null; + if (BuildConfig.DEBUG) + Log.d(TAG, "Disconnected from service"); + } + + }; + + private void startViewUpdaterIfNotRunning() { + if (!isUpdating) { + ViewUpdater updateThread = new ViewUpdater(this); + updateThread.start(); + } + } + + static class ViewUpdater extends Thread { + private static final String THREAD_NAME = "ViewUpdater"; + private PlayerWidgetService service; + + public ViewUpdater(PlayerWidgetService service) { + super(); + setName(THREAD_NAME); + this.service = service; + + } + + @Override + public void run() { + service.updateViews(); + } + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/app/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java new file mode 100644 index 000000000..1b93e6ea2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -0,0 +1,908 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * Provides methods for reading data from the AntennaPod database. + * In general, all database calls in DBReader-methods are executed on the caller's thread. + * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread. + * This class will use the {@link de.danoeh.antennapod.core.feed.EventDistributor} to notify listeners about changes in the database. + */ +public final class DBReader { + private static final String TAG = "DBReader"; + + /** + * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}. + */ + public static final int PLAYBACK_HISTORY_SIZE = 50; + + /** + * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}. + */ + public static final int DOWNLOAD_LOG_SIZE = 200; + + + private DBReader() { + } + + /** + * Returns a list of Feeds, sorted alphabetically by their title. + * + * @param context A context that is used for opening a database connection. + * @return A list of Feeds, sorted alphabetically by their title. A Feed-object + * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list + * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}. + */ + public static List getFeedList(final Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting Feedlist"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List result = getFeedList(adapter); + adapter.close(); + return result; + } + + private static List getFeedList(PodDBAdapter adapter) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting Feedlist"); + + Cursor feedlistCursor = adapter.getAllFeedsCursor(); + List feeds = new ArrayList(feedlistCursor.getCount()); + + if (feedlistCursor.moveToFirst()) { + do { + Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); + feeds.add(feed); + } while (feedlistCursor.moveToNext()); + } + feedlistCursor.close(); + return feeds; + } + + /** + * Returns a list with the download URLs of all feeds. + * + * @param context A context that is used for opening the database connection. + * @return A list of Strings with the download URLs of all feeds. + */ + public static List getFeedListDownloadUrls(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + List result = new ArrayList(); + adapter.open(); + Cursor feeds = adapter.getFeedCursorDownloadUrls(); + if (feeds.moveToFirst()) { + do { + result.add(feeds.getString(1)); + } while (feeds.moveToNext()); + } + feeds.close(); + adapter.close(); + + return result; + } + + /** + * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time. + * + * @param context A context that is used for opening a database connection. + * @param expirationTime Time that is used for determining whether a feed is outdated or not. + * A Feed is considered expired if 'lastUpdate < (currentTime - expirationTime)' evaluates to true. + * @return A list of Feeds, sorted alphabetically by their title. A Feed-object + * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list + * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}. + */ + public static List getExpiredFeedsList(final Context context, final long expirationTime) { + if (BuildConfig.DEBUG) + Log.d(TAG, String.format("getExpiredFeedsList(%d)", expirationTime)); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime); + List feeds = new ArrayList(feedlistCursor.getCount()); + + if (feedlistCursor.moveToFirst()) { + do { + Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); + feeds.add(feed); + } while (feedlistCursor.moveToNext()); + } + feedlistCursor.close(); + return feeds; + } + + /** + * Takes a list of FeedItems and loads their corresponding Feed-objects from the database. + * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will + * not find the correct feed of an item. + * + * @param context A context that is used for opening a database connection. + * @param items The FeedItems whose Feed-objects should be loaded. + */ + public static void loadFeedDataOfFeedItemlist(Context context, + List items) { + List feeds = getFeedList(context); + for (FeedItem item : items) { + for (Feed feed : feeds) { + if (feed.getId() == item.getFeedId()) { + item.setFeed(feed); + break; + } + } + if (item.getFeed() == null) { + Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId()); + } + } + } + + /** + * Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not + * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead. + * + * @param context A context that is used for opening a database connection. + * @param feed The Feed whose items should be loaded + * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly. + * The method does NOT change the items-attribute of the feed. + */ + public static List getFeedItemList(Context context, + final Feed feed) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed); + List items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + Collections.sort(items, new FeedItemPubdateComparator()); + + adapter.close(); + + for (FeedItem item : items) { + item.setFeed(feed); + } + + return items; + } + + static List extractItemlistFromCursor(Context context, Cursor itemlistCursor) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List result = extractItemlistFromCursor(adapter, itemlistCursor); + adapter.close(); + return result; + } + + private static List extractItemlistFromCursor( + PodDBAdapter adapter, Cursor itemlistCursor) { + ArrayList itemIds = new ArrayList(); + List items = new ArrayList( + itemlistCursor.getCount()); + + if (itemlistCursor.moveToFirst()) { + do { + FeedItem item = new FeedItem(); + + item.setId(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID)); + item.setTitle(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_TITLE)); + item.setLink(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_LINK)); + item.setPubDate(new Date(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE))); + item.setPaymentLink(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK)); + item.setFeedId(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_FEED)); + itemIds.add(String.valueOf(item.getId())); + + item.setRead((itemlistCursor + .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0)); + item.setItemIdentifier(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); + item.setFlattrStatus(new FlattrStatus(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS))); + + long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE); + if (imageIndex != 0) { + item.setImage(getFeedImage(adapter, imageIndex)); + } + + // extract chapters + boolean hasSimpleChapters = itemlistCursor + .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0; + if (hasSimpleChapters) { + Cursor chapterCursor = adapter + .getSimpleChaptersOfFeedItemCursor(item); + if (chapterCursor.moveToFirst()) { + item.setChapters(new ArrayList()); + do { + int chapterType = chapterCursor + .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX); + Chapter chapter = null; + long start = chapterCursor + .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX); + String title = chapterCursor + .getString(PodDBAdapter.KEY_TITLE_INDEX); + String link = chapterCursor + .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX); + + switch (chapterType) { + case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER: + chapter = new SimpleChapter(start, title, item, + link); + break; + case ID3Chapter.CHAPTERTYPE_ID3CHAPTER: + chapter = new ID3Chapter(start, title, item, + link); + break; + case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER: + chapter = new VorbisCommentChapter(start, + title, item, link); + break; + } + if (chapter != null) { + chapter.setId(chapterCursor + .getLong(PodDBAdapter.KEY_ID_INDEX)); + item.getChapters().add(chapter); + } + } while (chapterCursor.moveToNext()); + } + chapterCursor.close(); + } + items.add(item); + } while (itemlistCursor.moveToNext()); + } + + extractMediafromItemlist(adapter, items, itemIds); + return items; + } + + private static void extractMediafromItemlist(PodDBAdapter adapter, + List items, ArrayList itemIds) { + + List itemsCopy = new ArrayList(items); + Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds + .toArray(new String[itemIds.size()])); + if (cursor.moveToFirst()) { + do { + long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); + // find matching feed item + FeedItem item = getMatchingItemForMedia(itemId, itemsCopy); + if (item != null) { + item.setMedia(extractFeedMediaFromCursorRow(cursor)); + item.getMedia().setItem(item); + } + } while (cursor.moveToNext()); + cursor.close(); + } + } + + private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) { + long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + Date playbackCompletionDate = null; + long playbackCompletionTime = cursor + .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); + if (playbackCompletionTime > 0) { + playbackCompletionDate = new Date( + playbackCompletionTime); + } + + return new FeedMedia( + mediaId, + null, + cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), + cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), + cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), + cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), + cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), + cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), + cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, + playbackCompletionDate, + cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX)); + } + + private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, + Cursor cursor) { + Date lastUpdate = new Date( + cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_LASTUPDATE)); + + final FeedImage image; + long imageIndex = cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_IMAGE); + if (imageIndex != 0) { + image = getFeedImage(adapter, imageIndex); + } else { + image = null; + } + Feed feed = new Feed(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID), + lastUpdate, + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TITLE), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LINK), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DESCRIPTION), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_PAYMENT_LINK), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_AUTHOR), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LANGUAGE), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TYPE), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FEED_IDENTIFIER), + image, + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL), + cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0, + new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS))); + + if (image != null) { + image.setOwner(feed); + } + + FeedPreferences preferences = new FeedPreferences(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID), + cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0, + cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_USERNAME), + cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_PASSWORD)); + + feed.setPreferences(preferences); + return feed; + } + + private static FeedItem getMatchingItemForMedia(long itemId, + List items) { + for (FeedItem item : items) { + if (item.getId() == itemId) { + return item; + } + } + return null; + } + + static List getQueue(Context context, PodDBAdapter adapter) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting queue"); + + Cursor itemlistCursor = adapter.getQueueCursor(); + List items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + loadFeedDataOfFeedItemlist(context, items); + + return items; + } + + /** + * Loads the IDs of the FeedItems in the queue. This method should be preferred over + * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed. + * + * @param context A context that is used for opening a database connection. + * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned + * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. + */ + public static List getQueueIDList(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + + adapter.open(); + List result = getQueueIDList(adapter); + adapter.close(); + + return result; + } + + static List getQueueIDList(PodDBAdapter adapter) { + adapter.open(); + Cursor queueCursor = adapter.getQueueIDCursor(); + + List queueIds = new ArrayList(queueCursor.getCount()); + if (queueCursor.moveToFirst()) { + do { + queueIds.add(queueCursor.getLong(0)); + } while (queueCursor.moveToNext()); + } + return queueIds; + } + + + /** + * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using + * {@link #getQueueIDList(android.content.Context)} instead. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned + * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. + */ + public static List getQueue(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting queue"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List items = getQueue(context, adapter); + adapter.close(); + return items; + } + + /** + * Loads a list of FeedItems whose episode has been downloaded. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems whose episdoe has been downloaded. + */ + public static List getDownloadedItems(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting downloaded items"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getDownloadedItemsCursor(); + List items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + loadFeedDataOfFeedItemlist(context, items); + Collections.sort(items, new FeedItemPubdateComparator()); + + adapter.close(); + return items; + + } + + /** + * Loads a list of FeedItems whose 'read'-attribute is set to false. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used, + * consider using {@link #getUnreadItemIds(android.content.Context)} instead. + */ + public static List getUnreadItemsList(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting unread items list"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getUnreadItemsCursor(); + List items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + loadFeedDataOfFeedItemlist(context, items); + + adapter.close(); + + return items; + } + + /** + * Loads the IDs of the FeedItems whose 'read'-attribute is set to false. + * + * @param context A context that is used for opening a database connection. + * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred + * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used. + */ + public static long[] getUnreadItemIds(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getUnreadItemIdsCursor(); + long[] itemIds = new long[cursor.getCount()]; + int i = 0; + if (cursor.moveToFirst()) { + do { + itemIds[i] = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + i++; + } while (cursor.moveToNext()); + } + return itemIds; + } + + + /** + * Loads a list of FeedItems sorted by pubDate in descending order. + * + * @param context A context that is used for opening a database connection. + * @param limit The maximum number of episodes that should be loaded. + */ + public static List getRecentlyPublishedEpisodes(Context context, int limit) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting recently published items list"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getRecentlyPublishedItemsCursor(limit); + List items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + loadFeedDataOfFeedItemlist(context, items); + + adapter.close(); + + return items; + } + + /** + * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode + * has been completed at least once. + * + * @param context A context that is used for opening a database connection. + * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order. + * The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}. + */ + public static List getPlaybackHistory(final Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading playback history"); + final int PLAYBACK_HISTORY_SIZE = 50; + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE); + String[] itemIds = new String[mediaCursor.getCount()]; + for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) { + itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX)); + } + mediaCursor.close(); + Cursor itemCursor = adapter.getFeedItemCursor(itemIds); + List items = extractItemlistFromCursor(adapter, itemCursor); + loadFeedDataOfFeedItemlist(context, items); + itemCursor.close(); + adapter.close(); + + Collections.sort(items, new PlaybackCompletionDateComparator()); + return items; + } + + /** + * Loads the download log from the database. + * + * @param context A context that is used for opening a database connection. + * @return A list with DownloadStatus objects that represent the download log. + * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}. + */ + public static List getDownloadLog(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting DownloadLog"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE); + List downloadLog = new ArrayList( + logCursor.getCount()); + + if (logCursor.moveToFirst()) { + do { + long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + + long feedfileId = logCursor + .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); + int feedfileType = logCursor + .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); + boolean successful = logCursor + .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; + int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX); + String reasonDetailed = logCursor + .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); + String title = logCursor + .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); + Date completionDate = new Date( + logCursor + .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX) + ); + downloadLog.add(new DownloadStatus(id, title, feedfileId, + feedfileType, successful, DownloadError.fromCode(reason), completionDate, + reasonDetailed)); + + } while (logCursor.moveToNext()); + } + logCursor.close(); + Collections.sort(downloadLog, new DownloadStatusComparator()); + return downloadLog; + } + + /** + * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over + * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about + * the FeedItems is needed. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title. + */ + public static List getFeedStatisticsList(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List result = new ArrayList(); + Cursor cursor = adapter.getFeedStatisticsCursor(); + if (cursor.moveToFirst()) { + do { + result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES), + new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE)))); + } while (cursor.moveToNext()); + } + + cursor.close(); + adapter.close(); + return result; + } + + /** + * Loads a specific Feed from the database. + * + * @param context A context that is used for opening a database connection. + * @param feedId The ID of the Feed + * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the + * database and the items-attribute will be set correctly. + */ + public static Feed getFeed(final Context context, final long feedId) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Feed result = getFeed(context, feedId, adapter); + adapter.close(); + return result; + } + + static Feed getFeed(final Context context, final long feedId, PodDBAdapter adapter) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading feed with id " + feedId); + Feed feed = null; + + Cursor feedCursor = adapter.getFeedCursor(feedId); + if (feedCursor.moveToFirst()) { + feed = extractFeedFromCursorRow(adapter, feedCursor); + feed.setItems(getFeedItemList(context, feed)); + } else { + Log.e(TAG, "getFeed could not find feed with id " + feedId); + } + feedCursor.close(); + return feed; + } + + static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading feeditem with id " + itemId); + FeedItem item = null; + + Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId)); + if (itemCursor.moveToFirst()) { + List list = extractItemlistFromCursor(adapter, itemCursor); + if (list.size() > 0) { + item = list.get(0); + loadFeedDataOfFeedItemlist(context, list); + } + } + return item; + + } + + /** + * Loads a specific FeedItem from the database. + * + * @param context A context that is used for opening a database connection. + * @param itemId The ID of the FeedItem + * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes of the FeedItem will + * also be loaded from the database. + */ + public static FeedItem getFeedItem(final Context context, final long itemId) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading feeditem with id " + itemId); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedItem item = getFeedItem(context, itemId, adapter); + adapter.close(); + return item; + + } + + /** + * Loads additional information about a FeedItem, e.g. shownotes + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem + */ + public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor extraCursor = adapter.getExtraInformationOfItem(item); + if (extraCursor.moveToFirst()) { + String description = extraCursor + .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION); + String contentEncoded = extraCursor + .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED); + item.setDescription(description); + item.setContentEncoded(contentEncoded); + } + adapter.close(); + } + + /** + * Returns the number of downloaded episodes. + * + * @param context A context that is used for opening a database connection. + * @return The number of downloaded episodes. + */ + public static int getNumberOfDownloadedEpisodes(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final int result = adapter.getNumberOfDownloadedEpisodes(); + adapter.close(); + return result; + } + + /** + * Returns the number of unread items. + * + * @param context A context that is used for opening a database connection. + * @return The number of unread items. + */ + public static int getNumberOfUnreadItems(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final int result = adapter.getNumberOfUnreadItems(); + adapter.close(); + return result; + } + + /** + * Searches the DB for a FeedImage of the given id. + * + * @param context A context that is used for opening a database connection. + * @param imageId The id of the object + * @return The found object + */ + public static FeedImage getFeedImage(final Context context, final long imageId) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedImage result = getFeedImage(adapter, imageId); + adapter.close(); + return result; + } + + /** + * Searches the DB for a FeedImage of the given id. + * + * @param id The id of the object + * @return The found object + */ + static FeedImage getFeedImage(PodDBAdapter adapter, final long id) { + Cursor cursor = adapter.getImageCursor(id); + if ((cursor.getCount() == 0) || !cursor.moveToFirst()) { + throw new SQLException("No FeedImage found at index: " + id); + } + FeedImage image = new FeedImage(id, cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_TITLE)), + cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_FILE_URL)), + cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)), + cursor.getInt(cursor + .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0 + ); + cursor.close(); + return image; + } + + /** + * Searches the DB for a FeedMedia of the given id. + * + * @param context A context that is used for opening a database connection. + * @param mediaId The id of the object + * @return The found object + */ + public static FeedMedia getFeedMedia(final Context context, final long mediaId) { + PodDBAdapter adapter = new PodDBAdapter(context); + + adapter.open(); + Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId); + + FeedMedia media = null; + if (mediaCursor.moveToFirst()) { + final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); + media = extractFeedMediaFromCursorRow(mediaCursor); + FeedItem item = getFeedItem(context, itemId); + if (media != null && item != null) { + media.setItem(item); + item.setMedia(media); + } + } + + mediaCursor.close(); + adapter.close(); + + return media; + } + + /** + * Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems. + * + * @param context A context that is used for opening a database connection. + * @return The flattr queue as a List. + */ + public static List getFlattrQueue(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List result = new ArrayList(); + + // load feeds + Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor(); + if (feedCursor.moveToFirst()) { + do { + result.add(extractFeedFromCursorRow(adapter, feedCursor)); + } while (feedCursor.moveToNext()); + } + feedCursor.close(); + + //load feed items + Cursor feedItemCursor = adapter.getFeedItemsInFlattrQueueCursor(); + result.addAll(extractItemlistFromCursor(adapter, feedItemCursor)); + feedItemCursor.close(); + + adapter.close(); + Log.d(TAG, "Returning flattrQueueIterator for queue with " + result.size() + " items."); + return result; + } + + + /** + * Returns true if the flattr queue is empty. + * + * @param context A context that is used for opening a database connection. + */ + public static boolean getFlattrQueueEmpty(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + boolean empty = adapter.getFlattrQueueSize() == 0; + adapter.close(); + return empty; + } + + /** + * Returns data necessary for displaying the navigation drawer. This includes + * the list of subscriptions, the number of items in the queue and the number of unread + * items. + * + * @param context A context that is used for opening a database connection. + */ + public static NavDrawerData getNavDrawerData(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List feeds = getFeedList(adapter); + int queueSize = adapter.getQueueSize(); + int numUnreadItems = adapter.getNumberOfUnreadItems(); + NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems); + adapter.close(); + return result; + } + + public static class NavDrawerData { + public List feeds; + public int queueSize; + public int numUnreadItems; + + public NavDrawerData(List feeds, int queueSize, int numUnreadItems) { + this.feeds = feeds; + this.queueSize = queueSize; + this.numUnreadItems = numUnreadItems; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/app/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java new file mode 100644 index 000000000..28cab29b9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -0,0 +1,895 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.core.asynctask.FlattrStatusFetcher; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.GpodnetSyncService; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provides methods for doing common tasks that use DBReader and DBWriter. + */ +public final class DBTasks { + private static final String TAG = "DBTasks"; + + /** + * Executor service used by the autodownloadUndownloadedEpisodes method. + */ + private static ExecutorService autodownloadExec; + + static { + autodownloadExec = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + } + + private DBTasks() { + } + + /** + * Removes the feed with the given download url. This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the db + * @param downloadUrl URL of the feed. + */ + public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getFeedCursorDownloadUrls(); + long feedID = 0; + if (cursor.moveToFirst()) { + do { + if (cursor.getString(1).equals(downloadUrl)) { + feedID = cursor.getLong(0); + } + } while (cursor.moveToNext()); + } + cursor.close(); + adapter.close(); + + if (feedID != 0) { + try { + DBWriter.deleteFeed(context, feedID).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } else { + Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl); + } + } + + /** + * Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to + * start the {@link PlaybackService}. + * + * @param context Used for sending starting Services and Activities. + * @param media The FeedMedia object. + * @param showPlayer If true, starts the appropriate player activity ({@link de.danoeh.antennapod.activity.AudioplayerActivity} + * or {@link de.danoeh.antennapod.activity.VideoplayerActivity} + * @param startWhenPrepared Parameter for the {@link PlaybackService} start intent. If true, playback will start as + * soon as the PlaybackService has finished loading the FeedMedia object's file. + * @param shouldStream Parameter for the {@link PlaybackService} start intent. If true, the FeedMedia object's file + * will be streamed, otherwise the downloaded file will be used. If the downloaded file cannot be + * found, the PlaybackService will shutdown and the database entry of the FeedMedia object will be + * corrected. + */ + public static void playMedia(final Context context, final FeedMedia media, + boolean showPlayer, boolean startWhenPrepared, boolean shouldStream) { + try { + if (!shouldStream) { + if (media.fileExists() == false) { + throw new MediaFileNotFoundException( + "No episode was found at " + media.getFile_url(), + media); + } + } + // Start playback Service + Intent launchIntent = new Intent(context, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + startWhenPrepared); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, + shouldStream); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + context.startService(launchIntent); + if (showPlayer) { + // Launch media player + context.startActivity(PlaybackService.getPlayerActivityIntent( + context, media)); + } + DBWriter.addQueueItemAt(context, media.getItem().getId(), 0, false); + } catch (MediaFileNotFoundException e) { + e.printStackTrace(); + if (media.isPlaying()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + notifyMissingFeedMediaFile(context, media); + } + } + + private static AtomicBoolean isRefreshing = new AtomicBoolean(false); + + /** + * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still + * enqueuing Feeds for download from a previous call + * + * @param context Might be used for accessing the database + * @param feeds List of Feeds that should be refreshed. + */ + public static void refreshAllFeeds(final Context context, + final List feeds) { + if (isRefreshing.compareAndSet(false, true)) { + new Thread() { + public void run() { + if (feeds != null) { + refreshFeeds(context, feeds); + } else { + refreshFeeds(context, DBReader.getFeedList(context)); + } + isRefreshing.set(false); + + if (FlattrUtils.hasToken()) { + if (BuildConfig.DEBUG) Log.d(TAG, "Flattring all pending things."); + new FlattrClickWorker(context).executeAsync(); // flattr pending things + + if (BuildConfig.DEBUG) Log.d(TAG, "Fetching flattr status."); + new FlattrStatusFetcher(context).start(); + + } + GpodnetSyncService.sendSyncIntent(context); + autodownloadUndownloadedItems(context); + } + }.start(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Ignoring request to refresh all feeds: Refresh lock is locked"); + } + } + + /** + * Used by refreshExpiredFeeds to determine which feeds should be refreshed. + * This method will use the value specified in the UserPreferences as the + * expiration time. + * + * @param context Used for DB access. + * @return A list of expired feeds. An empty list will be returned if there + * are no expired feeds. + */ + public static List getExpiredFeeds(final Context context) { + long millis = UserPreferences.getUpdateInterval(); + + if (millis > 0) { + + List feedList = DBReader.getExpiredFeedsList(context, + millis); + if (feedList.size() > 0) { + refreshFeeds(context, feedList); + } + return feedList; + } else { + return new ArrayList(); + } + } + + /** + * Refreshes expired Feeds in the list returned by the getExpiredFeedsList(Context, long) method in DBReader. + * The expiration date parameter is determined by the update interval specified in {@link UserPreferences}. + * + * @param context Used for DB access. + */ + public static void refreshExpiredFeeds(final Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Refreshing expired feeds"); + + new Thread() { + public void run() { + refreshFeeds(context, getExpiredFeeds(context)); + } + }.start(); + } + + private static void refreshFeeds(final Context context, + final List feedList) { + + for (Feed feed : feedList) { + try { + refreshFeed(context, feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + context, + new DownloadStatus(feed, feed + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, false, e + .getMessage() + ) + ); + } + } + + } + + /** + * Updates a specific Feed. + * + * @param context Used for requesting the download. + * @param feed The Feed object. + */ + public static void refreshFeed(Context context, Feed feed) + throws DownloadRequestException { + Feed f; + if (feed.getPreferences() == null) { + f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle()); + } else { + f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle(), + feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); + } + f.setId(feed.getId()); + DownloadRequester.getInstance().downloadFeed(context, f); + } + + /** + * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file. + * + * @param context Used for requesting the download. + * @param image The FeedImage object. + */ + public static void notifyInvalidImageFile(final Context context, + final FeedImage image) { + Log.i(TAG, + "The DB was notified about an invalid image download. It will now try to re-download the image file"); + try { + DownloadRequester.getInstance().downloadImage(context, image); + } catch (DownloadRequestException e) { + e.printStackTrace(); + Log.w(TAG, "Failed to download invalid feed image"); + } + } + + /** + * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the + * DB and send a FeedUpdateBroadcast. + */ + public static void notifyMissingFeedMediaFile(final Context context, + final FeedMedia media) { + Log.i(TAG, + "The feedmanager was notified about a missing episode. It will update its database now."); + media.setDownloaded(false); + media.setFile_url(null); + DBWriter.setFeedMedia(context, media); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + + /** + * Request the download of all objects in the queue. from a separate Thread. + * + * @param context Used for requesting the download an accessing the database. + */ + public static void downloadAllItemsInQueue(final Context context) { + new Thread() { + public void run() { + List queue = DBReader.getQueue(context); + if (!queue.isEmpty()) { + try { + downloadFeedItems(context, + queue.toArray(new FeedItem[queue.size()])); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + } + }.start(); + } + + /** + * Requests the download of a list of FeedItem objects. + * + * @param context Used for requesting the download and accessing the DB. + * @param items The FeedItem objects. + */ + public static void downloadFeedItems(final Context context, + FeedItem... items) throws DownloadRequestException { + downloadFeedItems(true, context, items); + } + + private static void downloadFeedItems(boolean performAutoCleanup, + final Context context, final FeedItem... items) + throws DownloadRequestException { + final DownloadRequester requester = DownloadRequester.getInstance(); + + if (performAutoCleanup) { + new Thread() { + + @Override + public void run() { + performAutoCleanup(context, + getPerformAutoCleanupArgs(context, items.length)); + } + + }.start(); + } + for (FeedItem item : items) { + if (item.getMedia() != null + && !requester.isDownloadingFile(item.getMedia()) + && !item.getMedia().isDownloaded()) { + if (items.length > 1) { + try { + requester.downloadMedia(context, item.getMedia()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus(context, + new DownloadStatus(item.getMedia(), item + .getMedia() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage() + ) + ); + } + } else { + requester.downloadMedia(context, item.getMedia()); + } + } + } + } + + private static int getNumberOfUndownloadedEpisodes( + final List queue, final List unreadItems) { + int counter = 0; + for (FeedItem item : queue) { + if (item.hasMedia() && !item.getMedia().isDownloaded() + && !item.getMedia().isPlaying() + && item.getFeed().getPreferences().getAutoDownload()) { + counter++; + } + } + for (FeedItem item : unreadItems) { + if (item.hasMedia() && !item.getMedia().isDownloaded() + && item.getFeed().getPreferences().getAutoDownload()) { + counter++; + } + } + return counter; + } + + /** + * Looks for undownloaded episodes in the queue or list of unread items and request a download if + * 1. Network is available + * 2. There is free space in the episode cache + * This method is executed on an internal single thread executor. + * + * @param context Used for accessing the DB. + * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if + * its media ID is in the mediaIds list. + * @return A Future that can be used for waiting for the methods completion. + */ + public static Future autodownloadUndownloadedItems(final Context context, final long... mediaIds) { + return autodownloadExec.submit(new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Performing auto-dl of undownloaded episodes"); + if (NetworkUtils.autodownloadNetworkAvailable(context) + && UserPreferences.isEnableAutodownload()) { + final List queue = DBReader.getQueue(context); + final List unreadItems = DBReader + .getUnreadItemsList(context); + + int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue, + unreadItems); + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + int deletedEpisodes = performAutoCleanup(context, + getPerformAutoCleanupArgs(context, undownloadedEpisodes)); + int episodeSpaceLeft = undownloadedEpisodes; + boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences + .getEpisodeCacheSizeUnlimited(); + + if (!cacheIsUnlimited + && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes + + undownloadedEpisodes) { + episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() + - (downloadedEpisodes - deletedEpisodes); + } + + Arrays.sort(mediaIds); // sort for binary search + final boolean ignoreMediaIds = mediaIds.length == 0; + List itemsToDownload = new ArrayList(); + + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (int i = 0; i < queue.size(); i++) { // ignore playing item + FeedItem item = queue.get(i); + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() + && !item.getMedia().isPlaying() + && item.getFeed().getPreferences().getAutoDownload()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (FeedItem item : unreadItems) { + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() + && item.getFeed().getPreferences().getAutoDownload()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Enqueueing " + itemsToDownload.size() + + " items for download"); + + try { + downloadFeedItems(false, context, + itemsToDownload.toArray(new FeedItem[itemsToDownload + .size()]) + ); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + + } + } + }); + + } + + private static int getPerformAutoCleanupArgs(Context context, + final int episodeNumber) { + if (episodeNumber >= 0 + && UserPreferences.getEpisodeCacheSize() != UserPreferences + .getEpisodeCacheSizeUnlimited()) { + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + if (downloadedEpisodes + episodeNumber >= UserPreferences + .getEpisodeCacheSize()) { + + return downloadedEpisodes + episodeNumber + - UserPreferences.getEpisodeCacheSize(); + } + } + return 0; + } + + /** + * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller + * 'playbackCompletionDate'-value will be deleted first. + *

+ * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + */ + public static void performAutoCleanup(final Context context) { + performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0)); + } + + private static int performAutoCleanup(final Context context, + final int episodeNumber) { + List candidates = new ArrayList(); + List downloadedItems = DBReader.getDownloadedItems(context); + QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context)); + List delete; + for (FeedItem item : downloadedItems) { + if (item.hasMedia() && item.getMedia().isDownloaded() + && !queue.contains(item.getId()) && item.isRead()) { + candidates.add(item); + } + + } + + Collections.sort(candidates, new Comparator() { + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + Date l = lhs.getMedia().getPlaybackCompletionDate(); + Date r = rhs.getMedia().getPlaybackCompletionDate(); + + if (l == null) { + l = new Date(0); + } + if (r == null) { + r = new Date(0); + } + return l.compareTo(r); + } + }); + + if (candidates.size() > episodeNumber) { + delete = candidates.subList(0, episodeNumber); + } else { + delete = candidates; + } + + for (FeedItem item : delete) { + try { + DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + int counter = delete.size(); + + if (BuildConfig.DEBUG) + Log.d(TAG, String.format( + "Auto-delete deleted %d episodes (%d requested)", counter, + episodeNumber)); + + return counter; + } + + /** + * Adds all FeedItem objects whose 'read'-attribute is false to the queue in a separate thread. + */ + public static void enqueueAllNewItems(final Context context) { + long[] unreadItems = DBReader.getUnreadItemIds(context); + DBWriter.addQueueItem(context, unreadItems); + } + + /** + * Returns the successor of a FeedItem in the queue. + * + * @param context Used for accessing the DB. + * @param itemId ID of the FeedItem + * @param queue Used for determining the successor of the item. If this parameter is null, the method will load + * the queue from the database in the same thread. + * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor. + */ + public static FeedItem getQueueSuccessorOfItem(Context context, + final long itemId, List queue) { + FeedItem result = null; + if (queue == null) { + queue = DBReader.getQueue(context); + } + if (queue != null) { + Iterator iterator = queue.iterator(); + while (iterator.hasNext()) { + FeedItem item = iterator.next(); + if (item.getId() == itemId) { + if (iterator.hasNext()) { + result = iterator.next(); + } + break; + } + } + } + return result; + } + + /** + * Loads the queue from the database and checks if the specified FeedItem is in the queue. + * This method should NOT be executed in the GUI thread. + * + * @param context Used for accessing the DB. + * @param feedItemId ID of the FeedItem + */ + public static boolean isInQueue(Context context, final long feedItemId) { + List queue = DBReader.getQueueIDList(context); + return QueueAccess.IDListAccess(queue).contains(feedItemId); + } + + private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter, + Feed feed) { + if (feed.getId() != 0) { + return DBReader.getFeed(context, feed.getId(), adapter); + } else { + List feeds = DBReader.getFeedList(context); + for (Feed f : feeds) { + if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { + f.setItems(DBReader.getFeedItemList(context, f)); + return f; + } + } + } + return null; + } + + /** + * Get a FeedItem by its identifying value. + */ + private static FeedItem searchFeedItemByIdentifyingValue(Feed feed, + String identifier) { + for (FeedItem item : feed.getItems()) { + if (item.getIdentifyingValue().equals(identifier)) { + return item; + } + } + return null; + } + + /** + * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same + * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed. + * These FeedItems will be marked as unread. + *

+ * This method can update multiple feeds at once. Submitting a feed twice in the same method call can result in undefined behavior. + *

+ * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + * @param newFeeds The new Feed objects. + * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise. + */ + public static synchronized Feed[] updateFeed(final Context context, + final Feed... newFeeds) { + List newFeedsList = new ArrayList(); + List updatedFeedsList = new ArrayList(); + Feed[] resultFeeds = new Feed[newFeeds.length]; + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) { + + final Feed newFeed = newFeeds[feedIdx]; + + // Look up feed in the feedslist + final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter, + newFeed); + if (savedFeed == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Found no existing Feed with title " + + newFeed.getTitle() + ". Adding as new one." + ); + // Add a new Feed + newFeedsList.add(newFeed); + resultFeeds[feedIdx] = newFeed; + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Feed with title " + newFeed.getTitle() + + " already exists. Syncing new with existing one."); + + Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); + if (savedFeed.compareWithOther(newFeed)) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Feed has updated attribute values. Updating old feed's attributes"); + savedFeed.updateFromOther(newFeed); + } + if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); + savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); + } + // Look for new or updated Items + for (int idx = 0; idx < newFeed.getItems().size(); idx++) { + final FeedItem item = newFeed.getItems().get(idx); + FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, + item.getIdentifyingValue()); + if (oldItem == null) { + // item is new + final int i = idx; + item.setFeed(savedFeed); + savedFeed.getItems().add(i, item); + item.setRead(false); + } else { + oldItem.updateFromOther(item); + } + } + // update attributes + savedFeed.setLastUpdate(newFeed.getLastUpdate()); + savedFeed.setType(newFeed.getType()); + + updatedFeedsList.add(savedFeed); + resultFeeds[feedIdx] = savedFeed; + } + } + + adapter.close(); + + try { + DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get(); + DBWriter.setCompleteFeed(context, updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + + return resultFeeds; + } + + /** + * Searches the titles of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string. + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask> searchFeedItemTitle(final Context context, + final long feedID, final String query) { + return new FutureTask>(new QueryTask>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemTitles(feedID, + query); + List items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches the descriptions of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask> searchFeedItemDescription(final Context context, + final long feedID, final String query) { + return new FutureTask>(new QueryTask>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemDescriptions(feedID, + query); + List items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches the contentEncoded-value of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask> searchFeedItemContentEncoded(final Context context, + final long feedID, final String query) { + return new FutureTask>(new QueryTask>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemContentEncoded(feedID, + query); + List items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches chapters of the FeedItems of a specific Feed for a given string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask> searchFeedItemChapters(final Context context, + final long feedID, final String query) { + return new FutureTask>(new QueryTask>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemChapters(feedID, + query); + List items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * A runnable which should be used for database queries. The onCompletion + * method is executed on the database executor to handle Cursors correctly. + * This class automatically creates a PodDBAdapter object and closes it when + * it is no longer in use. + */ + static abstract class QueryTask implements Callable { + private T result; + private Context context; + + public QueryTask(Context context) { + this.context = context; + } + + @Override + public T call() throws Exception { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + execute(adapter); + adapter.close(); + return result; + } + + public abstract void execute(PodDBAdapter adapter); + + protected void setResult(T result) { + this.result = result; + } + } + + /** + * Adds the given FeedItem to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * + * @param context + * @param item + */ + public static void flattrItemIfLoggedIn(Context context, FeedItem item) { + if (FlattrUtils.hasToken()) { + item.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, item, true); + } else { + FlattrUtils.showNoTokenDialogOrRedirect(context, item.getPaymentLink()); + } + } + + /** + * Adds the given Feed to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * + * @param context + * @param feed + */ + public static void flattrFeedIfLoggedIn(Context context, Feed feed) { + if (FlattrUtils.hasToken()) { + feed.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, feed, true); + } else { + FlattrUtils.showNoTokenDialogOrRedirect(context, feed.getPaymentLink()); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/app/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java new file mode 100644 index 000000000..225f74c96 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -0,0 +1,974 @@ +package de.danoeh.antennapod.core.storage; + +import android.app.backup.BackupManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.preference.PreferenceManager; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; +import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; +import org.shredzone.flattr4j.model.Flattr; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; + +/** + * Provides methods for writing data to AntennaPod's database. + * In general, DBWriter-methods will be executed on an internal ExecutorService. + * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's + * will NOT contain any results. + * The caller can also use the {@link EventDistributor} in order to be notified about the method's completion asynchronously. + * This class will use the {@link EventDistributor} to notify listeners about changes in the database. + */ +public class DBWriter { + private static final String TAG = "DBWriter"; + + private static final ExecutorService dbExec; + + static { + dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + } + + private DBWriter() { + } + + /** + * Deletes a downloaded FeedMedia file from the storage device. + * + * @param context A context that is used for opening a database connection. + * @param mediaId ID of the FeedMedia object whose downloaded file should be deleted. + */ + public static Future deleteFeedMediaOfItem(final Context context, + final long mediaId) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + + final FeedMedia media = DBReader.getFeedMedia(context, mediaId); + if (media != null) { + Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s", + media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded()))); + boolean result = false; + if (media.isDownloaded()) { + // delete downloaded media file + File mediaFile = new File(media.getFile_url()); + if (mediaFile.exists()) { + result = mediaFile.delete(); + } + media.setDownloaded(false); + media.setFile_url(null); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setMedia(media); + adapter.close(); + + // If media is currently being played, change playback + // type to 'stream' and shutdown playback service + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) { + if (media.getId() == PlaybackPreferences + .getCurrentlyPlayingFeedMediaId()) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, + true); + editor.commit(); + } + if (PlaybackPreferences + .getCurrentlyPlayingFeedMediaId() == media + .getId()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting File. Result: " + result); + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + } + }); + } + + /** + * Deletes a Feed and all downloaded files of its components like images and downloaded episodes. + * + * @param context A context that is used for opening a database connection. + * @param feedId ID of the Feed that should be deleted. + */ + public static Future deleteFeed(final Context context, final long feedId) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + DownloadRequester requester = DownloadRequester.getInstance(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context + .getApplicationContext()); + final Feed feed = DBReader.getFeed(context, feedId); + if (feed != null) { + if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getLastPlayedFeedId() == feed + .getId()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + -1); + editor.commit(); + } + + // delete image file + if (feed.getImage() != null) { + if (feed.getImage().isDownloaded() + && feed.getImage().getFile_url() != null) { + File imageFile = new File(feed.getImage() + .getFile_url()); + imageFile.delete(); + } else if (requester.isDownloadingFile(feed.getImage())) { + requester.cancelDownload(context, feed.getImage()); + } + } + // delete stored media files and mark them as read + List queue = DBReader.getQueue(context); + boolean queueWasModified = false; + if (feed.getItems() == null) { + DBReader.getFeedItemList(context, feed); + } + + for (FeedItem item : feed.getItems()) { + queueWasModified |= queue.remove(item); + if (item.getMedia() != null + && item.getMedia().isDownloaded()) { + File mediaFile = new File(item.getMedia() + .getFile_url()); + mediaFile.delete(); + } else if (item.getMedia() != null + && requester.isDownloadingFile(item.getMedia())) { + requester.cancelDownload(context, item.getMedia()); + } + + if (item.hasItemImage()) { + FeedImage image = item.getImage(); + if (image.isDownloaded() && image.getFile_url() != null) { + File imgFile = new File(image.getFile_url()); + imgFile.delete(); + } else if (requester.isDownloadingFile(image)) { + requester.cancelDownload(context, item.getImage()); + } + } + } + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + if (queueWasModified) { + adapter.setQueue(queue); + } + adapter.removeFeed(feed); + adapter.close(); + + GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); + } + } + }); + } + + /** + * Deletes the entire playback history. + * + * @param context A context that is used for opening a database connection. + */ + public static Future clearPlaybackHistory(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearPlaybackHistory(); + adapter.close(); + EventDistributor.getInstance() + .sendPlaybackHistoryUpdateBroadcast(); + } + }); + } + + /** + * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if + * its playback completion date is set to a non-null value. This method will set the playback completion date to the + * current date regardless of the current value. + * + * @param context A context that is used for opening a database connection. + * @param media FeedMedia that should be added to the playback history. + */ + public static Future addItemToPlaybackHistory(final Context context, + final FeedMedia media) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Adding new item to playback history"); + media.setPlaybackCompletionDate(new Date()); + // reset played_duration to 0 so that it behaves correctly when the episode is played again + media.setPlayedDuration(0); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedMediaPlaybackCompletionDate(media); + adapter.close(); + EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); + + } + }); + } + + private static void cleanupDownloadLog(final PodDBAdapter adapter) { + final long logSize = adapter.getDownloadLogSize(); + if (logSize > DBReader.DOWNLOAD_LOG_SIZE) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Cleaning up download log"); + adapter.removeDownloadLogItems(logSize - DBReader.DOWNLOAD_LOG_SIZE); + } + } + + /** + * Adds a Download status object to the download log. + * + * @param context A context that is used for opening a database connection. + * @param status The DownloadStatus object. + */ + public static Future addDownloadStatus(final Context context, + final DownloadStatus status) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setDownloadStatus(status); + adapter.close(); + EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); + } + }); + + } + + /** + * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to + * true. If the FeedItem is already in the queue, the queue will not be modified. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem that should be added to the queue. + * @param index Destination index. Must be in range 0..queue.size() + * @param performAutoDownload True if an auto-download process should be started after the operation + * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size() + */ + public static Future addQueueItemAt(final Context context, final long itemId, + final int index, final boolean performAutoDownload) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List queue = DBReader + .getQueue(context, adapter); + FeedItem item = null; + + if (queue != null) { + boolean queueModified = false; + boolean unreadItemsModified = false; + + if (!itemListContains(queue, itemId)) { + item = DBReader.getFeedItem(context, itemId); + if (item != null) { + queue.add(index, item); + queueModified = true; + if (!item.isRead()) { + item.setRead(true); + unreadItemsModified = true; + } + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + if (unreadItemsModified && item != null) { + adapter.setSingleFeedItem(item); + EventDistributor.getInstance() + .sendUnreadItemsUpdateBroadcast(); + } + } + adapter.close(); + if (performAutoDownload) { + DBTasks.autodownloadUndownloadedItems(context); + } + + } + }); + + } + + /** + * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. + * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemIds IDs of the FeedItem objects that should be added to the queue. + */ + public static Future addQueueItem(final Context context, + final long... itemIds) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + if (itemIds.length > 0) { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List queue = DBReader.getQueue(context, + adapter); + + if (queue != null) { + boolean queueModified = false; + boolean unreadItemsModified = false; + List itemsToSave = new LinkedList(); + for (int i = 0; i < itemIds.length; i++) { + if (!itemListContains(queue, itemIds[i])) { + final FeedItem item = DBReader.getFeedItem( + context, itemIds[i]); + + if (item != null) { + queue.add(item); + queueModified = true; + if (!item.isRead()) { + item.setRead(true); + itemsToSave.add(item); + unreadItemsModified = true; + } + } + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + if (unreadItemsModified) { + adapter.setFeedItemlist(itemsToSave); + EventDistributor.getInstance() + .sendUnreadItemsUpdateBroadcast(); + } + } + adapter.close(); + DBTasks.autodownloadUndownloadedItems(context); + } + } + }); + + } + + /** + * Removes all FeedItem objects from the queue. + * + * @param context A context that is used for opening a database connection. + */ + public static Future clearQueue(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearQueue(); + adapter.close(); + + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + } + }); + } + + /** + * Removes a FeedItem object from the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem that should be removed. + * @param performAutoDownload true if an auto-download process should be started after the operation. + */ + public static Future removeQueueItem(final Context context, + final long itemId, final boolean performAutoDownload) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List queue = DBReader + .getQueue(context, adapter); + FeedItem item = null; + + if (queue != null) { + boolean queueModified = false; + QueueAccess queueAccess = QueueAccess.ItemListAccess(queue); + if (queueAccess.contains(itemId)) { + item = DBReader.getFeedItem(context, itemId); + if (item != null) { + queueModified = queueAccess.remove(itemId); + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } else { + Log.w(TAG, "Queue was not modified by call to removeQueueItem"); + } + } else { + Log.e(TAG, "removeQueueItem: Could not load queue"); + } + adapter.close(); + if (performAutoDownload) { + DBTasks.autodownloadUndownloadedItems(context); + } + } + }); + + } + + /** + * Moves the specified item to the top of the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemId The item to move to the top of the queue + * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to + * false if the caller wants to avoid unexpected updates of the GUI. + */ + public static Future moveQueueItemToTop(final Context context, final long itemId, final boolean broadcastUpdate) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + List queueIdList = DBReader.getQueueIDList(context); + int currentLocation = 0; + for (long id : queueIdList) { + if (id == itemId) { + moveQueueItemHelper(context, currentLocation, 0, broadcastUpdate); + return; + } + currentLocation++; + } + Log.e(TAG, "moveQueueItemToTop: item not found"); + } + }); + } + + /** + * Moves the specified item to the bottom of the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemId The item to move to the bottom of the queue + * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to + * false if the caller wants to avoid unexpected updates of the GUI. + */ + public static Future moveQueueItemToBottom(final Context context, final long itemId, + final boolean broadcastUpdate) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + List queueIdList = DBReader.getQueueIDList(context); + int currentLocation = 0; + for (long id : queueIdList) { + if (id == itemId) { + moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1, + broadcastUpdate); + return; + } + currentLocation++; + } + Log.e(TAG, "moveQueueItemToBottom: item not found"); + } + }); + } + + /** + * Changes the position of a FeedItem in the queue. + * + * @param context A context that is used for opening a database connection. + * @param from Source index. Must be in range 0..queue.size()-1. + * @param to Destination index. Must be in range 0..queue.size()-1. + * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to + * false if the caller wants to avoid unexpected updates of the GUI. + * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) + */ + public static Future moveQueueItem(final Context context, final int from, + final int to, final boolean broadcastUpdate) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + moveQueueItemHelper(context, from, to, broadcastUpdate); + } + }); + } + + /** + * Changes the position of a FeedItem in the queue. + *

+ * This function must be run using the ExecutorService (dbExec). + * + * @param context A context that is used for opening a database connection. + * @param from Source index. Must be in range 0..queue.size()-1. + * @param to Destination index. Must be in range 0..queue.size()-1. + * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to + * false if the caller wants to avoid unexpected updates of the GUI. + * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) + */ + private static void moveQueueItemHelper(final Context context, final int from, + final int to, final boolean broadcastUpdate) { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List queue = DBReader + .getQueue(context, adapter); + + if (queue != null) { + if (from >= 0 && from < queue.size() && to >= 0 + && to < queue.size()) { + + final FeedItem item = queue.remove(from); + queue.add(to, item); + + adapter.setQueue(queue); + if (broadcastUpdate) { + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + + } + } else { + Log.e(TAG, "moveQueueItemHelper: Could not load queue"); + } + adapter.close(); + } + + /** + * Sets the 'read'-attribute of a FeedItem to the specified value. + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem object + * @param read New value of the 'read'-attribute + * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object. + * If the FeedItem has no FeedMedia object, this parameter will be ignored. + */ + public static Future markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) { + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0; + return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition); + } + + /** + * Sets the 'read'-attribute of a FeedItem to the specified value. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem + * @param read New value of the 'read'-attribute + */ + public static Future markItemRead(final Context context, final long itemId, + final boolean read) { + return markItemRead(context, itemId, read, 0, false); + } + + private static Future markItemRead(final Context context, final long itemId, + final boolean read, final long mediaId, + final boolean resetMediaPosition) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemRead(read, itemId, mediaId, + resetMediaPosition); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + } + + /** + * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. + * + * @param context A context that is used for opening a database connection. + * @param feedId ID of the Feed. + */ + public static Future markFeedRead(final Context context, final long feedId) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + itemCursor.moveToNext(); + } + itemCursor.close(); + adapter.setFeedItemRead(true, itemIds); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + + } + + /** + * Sets the 'read'-attribute of all FeedItems to true. + * + * @param context A context that is used for opening a database connection. + */ + public static Future markAllItemsRead(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor itemCursor = adapter.getUnreadItemsCursor(); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + itemCursor.moveToNext(); + } + itemCursor.close(); + adapter.setFeedItemRead(true, itemIds); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + + } + + static Future addNewFeed(final Context context, final Feed... feeds) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feeds); + adapter.close(); + + for (Feed feed : feeds) { + GpodnetPreferences.addAddedFeed(feed.getDownload_url()); + } + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); + } + }); + } + + static Future setCompleteFeed(final Context context, final Feed... feeds) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feeds); + adapter.close(); + + } + }); + + } + + /** + * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The + * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved. + * + * @param context A context that is used for opening a database connection. + * @param media The FeedMedia object. + */ + public static Future setFeedMedia(final Context context, + final FeedMedia media) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setMedia(media); + adapter.close(); + } + }); + } + + /** + * Saves the 'position' and 'duration' attributes of a FeedMedia object + * + * @param context A context that is used for opening a database connection. + * @param media The FeedMedia object. + */ + public static Future setFeedMediaPlaybackInformation(final Context context, final FeedMedia media) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedMediaPlaybackInformation(media); + adapter.close(); + } + }); + } + + /** + * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including + * the content of FeedComponent-attributes. + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem object. + */ + public static Future setFeedItem(final Context context, + final FeedItem item) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setSingleFeedItem(item); + adapter.close(); + } + }); + } + + /** + * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The + * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved. + * + * @param context A context that is used for opening a database connection. + * @param image The FeedImage object. + */ + public static Future setFeedImage(final Context context, + final FeedImage image) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setImage(image); + adapter.close(); + } + }); + } + + /** + * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed + * and the value is the updated URL + */ + public static Future updateFeedDownloadURLs(final Context context, final Map urls) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (String key : urls.keySet()) { + if (BuildConfig.DEBUG) Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key)); + + adapter.setFeedDownloadUrl(key, urls.get(key)); + } + adapter.close(); + } + }); + } + + /** + * Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0. + * + * @param context Used for opening a database connection. + * @param preferences The FeedPreferences object. + */ + public static Future setFeedPreferences(final Context context, final FeedPreferences preferences) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedPreferences(preferences); + adapter.close(); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + }); + } + + private static boolean itemListContains(List items, long itemId) { + for (FeedItem item : items) { + if (item.getId() == itemId) { + return true; + } + } + return false; + } + + /** + * Saves the FlattrStatus of a FeedItem object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + public static Future setFeedItemFlattrStatus(final Context context, + final FeedItem item, + final boolean startFlattrClickWorker) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemFlattrStatus(item); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context).executeAsync(); + } + } + }); + } + + /** + * Saves the FlattrStatus of a Feed object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + private static Future setFeedFlattrStatus(final Context context, + final Feed feed, + final boolean startFlattrClickWorker) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedFlattrStatus(feed); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context).executeAsync(); + } + } + }); + } + + /** + * format an url for querying the database + * (postfix a / and apply percent-encoding) + */ + private static String formatURIForQuery(String uri) { + try { + return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getMessage()); + return ""; + } + } + + + /** + * Set flattr status of the passed thing (either a FeedItem or a Feed) + * + * @param context + * @param thing + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + * @return + */ + public static Future setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) { + // must propagate this to back db + if (thing instanceof FeedItem) + return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker); + else if (thing instanceof Feed) + return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker); + else if (thing instanceof SimpleFlattrThing) { + } // SimpleFlattrThings are generated on the fly and do not have DB backing + else + Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing"); + + return null; + } + + /** + * Reset flattr status to unflattrd for all items + */ + public static Future clearAllFlattrStatus(final Context context) { + Log.d(TAG, "clearAllFlattrStatus()"); + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearAllFlattrStatus(); + adapter.close(); + } + }); + } + + /** + * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp, + * where the information has been retrieved from the flattr API + */ + public static Future setFlattredStatus(final Context context, final List flattrList) { + Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items"); + // clear flattr status in db + clearAllFlattrStatus(context); + + // submit list with flattred things having normalized URLs to db + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (Flattr flattr : flattrList) { + adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime())); + } + adapter.close(); + } + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java b/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java new file mode 100644 index 000000000..c85559e20 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.storage; + +/** + * Thrown by the DownloadRequester if a download request contains invalid data + * or something went wrong while processing the request. + */ +public class DownloadRequestException extends Exception { + + public DownloadRequestException() { + super(); + } + + public DownloadRequestException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public DownloadRequestException(String detailMessage) { + super(detailMessage); + } + + public DownloadRequestException(Throwable throwable) { + super(throwable); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java new file mode 100644 index 000000000..c313055a5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -0,0 +1,366 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.webkit.URLUtil; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.URLChecker; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Sends download requests to the DownloadService. This class should always be used for starting downloads, + * otherwise they won't work correctly. + */ +public class DownloadRequester { + private static final String TAG = "DownloadRequester"; + + public static final String IMAGE_DOWNLOADPATH = "images/"; + public static final String FEED_DOWNLOADPATH = "cache/"; + public static final String MEDIA_DOWNLOADPATH = "media/"; + + private static DownloadRequester downloader; + + Map downloads; + + private DownloadRequester() { + downloads = new ConcurrentHashMap(); + } + + public static synchronized DownloadRequester getInstance() { + if (downloader == null) { + downloader = new DownloadRequester(); + } + return downloader; + } + + /** + * Starts a new download with the given DownloadRequest. This method should only + * be used from outside classes if the DownloadRequest was created by the DownloadService to + * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead. + * + * @param context Context object for starting the DownloadService + * @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method + * call will return false. + * @return True if the download request was accepted, false otherwise. + */ + public boolean download(Context context, DownloadRequest request) { + Validate.notNull(context); + Validate.notNull(request); + + if (downloads.containsKey(request.getSource())) { + if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); + return false; + } + downloads.put(request.getSource(), request); + + Intent launchIntent = new Intent(context, DownloadService.class); + launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); + context.startService(launchIntent); + EventDistributor.getInstance().sendDownloadQueuedBroadcast(); + return true; + } + + private void download(Context context, FeedFile item, File dest, + boolean overwriteIfExists, String username, String password, boolean deleteOnFailure) { + if (!isDownloadingFile(item)) { + if (!isFilenameAvailable(dest.toString()) || (deleteOnFailure && dest.exists())) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Filename already used."); + if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { + boolean result = dest.delete(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting file. Result: " + result); + } else { + // find different name + File newDest = null; + for (int i = 1; i < Integer.MAX_VALUE; i++) { + String newName = FilenameUtils.getBaseName(dest + .getName()) + + "-" + + i + + FilenameUtils.EXTENSION_SEPARATOR + + FilenameUtils.getExtension(dest.getName()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Testing filename " + newName); + newDest = new File(dest.getParent(), newName); + if (!newDest.exists() + && isFilenameAvailable(newDest.toString())) { + if (BuildConfig.DEBUG) + Log.d(TAG, "File doesn't exist yet. Using " + + newName); + break; + } + } + if (newDest != null) { + dest = newDest; + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, + "Requesting download of url " + item.getDownload_url()); + item.setDownload_url(URLChecker.prepareURL(item.getDownload_url())); + + DownloadRequest request = new DownloadRequest(dest.toString(), + URLChecker.prepareURL(item.getDownload_url()), item.getHumanReadableIdentifier(), + item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure); + + download(context, request); + } else { + Log.e(TAG, "URL " + item.getDownload_url() + + " is already being downloaded"); + } + } + + /** + * Returns true if a filename is available and false if it has already been + * taken by another requested download. + */ + private boolean isFilenameAvailable(String path) { + for (String key : downloads.keySet()) { + DownloadRequest r = downloads.get(key); + if (StringUtils.equals(r.getDestination(), path)) { + if (BuildConfig.DEBUG) + Log.d(TAG, path + + " is already used by another requested download"); + return false; + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, path + " is available as a download destination"); + return true; + } + + public void downloadFeed(Context context, Feed feed) + throws DownloadRequestException { + if (feedFileValid(feed)) { + String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; + String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; + + download(context, feed, new File(getFeedfilePath(context), + getFeedfileName(feed)), true, username, password, true); + } + } + + public void downloadImage(Context context, FeedImage image) + throws DownloadRequestException { + if (feedFileValid(image)) { + download(context, image, new File(getImagefilePath(context), + getImagefileName(image)), false, null, null, false); + } + } + + public void downloadMedia(Context context, FeedMedia feedmedia) + throws DownloadRequestException { + if (feedFileValid(feedmedia)) { + Feed feed = feedmedia.getItem().getFeed(); + String username; + String password; + if (feed != null && feed.getPreferences() != null) { + username = feed.getPreferences().getUsername(); + password = feed.getPreferences().getPassword(); + } else { + username = null; + password = null; + } + + File dest; + if (feedmedia.getFile_url() != null) { + dest = new File(feedmedia.getFile_url()); + } else { + dest = new File(getMediafilePath(context, feedmedia), + getMediafilename(feedmedia)); + } + download(context, feedmedia, + dest, false, username, password, false + ); + } + } + + /** + * Throws a DownloadRequestException if the feedfile or the download url of + * the feedfile is null. + * + * @throws DownloadRequestException + */ + private boolean feedFileValid(FeedFile f) throws DownloadRequestException { + if (f == null) { + throw new DownloadRequestException("Feedfile was null"); + } else if (f.getDownload_url() == null) { + throw new DownloadRequestException("File has no download URL"); + } else { + return true; + } + } + + /** + * Cancels a running download. + */ + public void cancelDownload(final Context context, final FeedFile f) { + cancelDownload(context, f.getDownload_url()); + } + + /** + * Cancels a running download. + */ + public void cancelDownload(final Context context, final String downloadUrl) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelling download with url " + downloadUrl); + Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); + cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl); + context.sendBroadcast(cancelIntent); + } + + /** + * Cancels all running downloads + */ + public void cancelAllDownloads(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelling all running downloads"); + context.sendBroadcast(new Intent( + DownloadService.ACTION_CANCEL_ALL_DOWNLOADS)); + } + + /** + * Returns true if there is at least one Feed in the downloads queue. + */ + public boolean isDownloadingFeeds() { + for (DownloadRequest r : downloads.values()) { + if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + return true; + } + } + return false; + } + + /** + * Checks if feedfile is in the downloads list + */ + public boolean isDownloadingFile(FeedFile item) { + if (item.getDownload_url() != null) { + return downloads.containsKey(item.getDownload_url()); + } + return false; + } + + public DownloadRequest getDownload(String downloadUrl) { + return downloads.get(downloadUrl); + } + + /** + * Checks if feedfile with the given download url is in the downloads list + */ + public boolean isDownloadingFile(String downloadUrl) { + return downloads.get(downloadUrl) != null; + } + + public boolean hasNoDownloads() { + return downloads.isEmpty(); + } + + /** + * Remove an object from the downloads-list of the requester. + */ + public void removeDownload(DownloadRequest r) { + if (downloads.remove(r.getSource()) == null) { + Log.e(TAG, + "Could not remove object with url " + r.getSource()); + } + } + + /** + * Get the number of uncompleted Downloads + */ + public int getNumberOfDownloads() { + return downloads.size(); + } + + public String getFeedfilePath(Context context) + throws DownloadRequestException { + return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) + .toString() + "/"; + } + + public String getFeedfileName(Feed feed) { + String filename = feed.getDownload_url(); + if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { + filename = feed.getTitle(); + } + return "feed-" + FileNameGenerator.generateFileName(filename); + } + + public String getImagefilePath(Context context) + throws DownloadRequestException { + return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH) + .toString() + "/"; + } + + public String getImagefileName(FeedImage image) { + String filename = image.getDownload_url(); + if (image.getOwner() != null && image.getOwner().getHumanReadableIdentifier() != null) { + filename = image.getOwner().getHumanReadableIdentifier(); + } + return "image-" + FileNameGenerator.generateFileName(filename); + } + + public String getMediafilePath(Context context, FeedMedia media) + throws DownloadRequestException { + File externalStorage = getExternalFilesDirOrThrowException( + context, + MEDIA_DOWNLOADPATH + + FileNameGenerator.generateFileName(media.getItem() + .getFeed().getTitle()) + "/" + ); + return externalStorage.toString(); + } + + private File getExternalFilesDirOrThrowException(Context context, + String type) throws DownloadRequestException { + File result = UserPreferences.getDataFolder(context, type); + if (result == null) { + throw new DownloadRequestException( + "Failed to access external storage"); + } + return result; + } + + public String getMediafilename(FeedMedia media) { + String filename; + String titleBaseFilename = ""; + + // Try to generate the filename by the item title + if (media.getItem() != null && media.getItem().getTitle() != null) { + String title = media.getItem().getTitle(); + // Delete reserved characters + titleBaseFilename = title.replaceAll("[\\\\/%\\?\\*:|<>\"\\p{Cntrl}]", ""); + titleBaseFilename = titleBaseFilename.trim(); + } + + String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(), + null, media.getMime_type()); + ; + + if (titleBaseFilename != "") { + // Append extension + filename = titleBaseFilename + FilenameUtils.EXTENSION_SEPARATOR + + FilenameUtils.getExtension(URLBaseFilename); + } else { + // Fall back on URL file name + filename = URLBaseFilename; + } + return filename; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java b/app/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java new file mode 100644 index 000000000..f6a59836b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java @@ -0,0 +1,70 @@ +package de.danoeh.antennapod.core.storage; + +import java.util.Date; + +/** + * Contains information about a feed's items. + */ +public class FeedItemStatistics { + private long feedID; + private int numberOfItems; + private int numberOfNewItems; + private int numberOfInProgressItems; + private Date lastUpdate; + private static final Date UNKNOWN_DATE = new Date(0); + + + /** + * Creates new FeedItemStatistics object. + * + * @param feedID ID of the feed. + * @param numberOfItems Number of items that this feed has. + * @param numberOfNewItems Number of unread items this feed has. + * @param numberOfInProgressItems Number of items that the user has started listening to. + * @param lastUpdate pubDate of the latest episode. A lastUpdate value of 0 will be interpreted as DATE_UNKOWN if + * numberOfItems is 0. + */ + public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) { + this.feedID = feedID; + this.numberOfItems = numberOfItems; + this.numberOfNewItems = numberOfNewItems; + this.numberOfInProgressItems = numberOfInProgressItems; + if (numberOfItems > 0) { + this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } else { + this.lastUpdate = UNKNOWN_DATE; + } + } + + public long getFeedID() { + return feedID; + } + + public int getNumberOfItems() { + return numberOfItems; + } + + public int getNumberOfNewItems() { + return numberOfNewItems; + } + + public int getNumberOfInProgressItems() { + return numberOfInProgressItems; + } + + /** + * Returns the pubDate of the latest item in the feed. Users of this method + * should check if this value is unkown or not by calling lastUpdateKnown() first. + */ + public Date getLastUpdate() { + return (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } + + /** + * Returns true if the lastUpdate value is known. The lastUpdate value is unkown if the + * feed has no items. + */ + public boolean lastUpdateKnown() { + return lastUpdate != UNKNOWN_DATE; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java b/app/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java new file mode 100644 index 000000000..41b379471 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.SearchResult; +import de.danoeh.antennapod.core.util.comparator.SearchResultValueComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * Performs search on Feeds and FeedItems + */ +public class FeedSearcher { + private static final String TAG = "FeedSearcher"; + + + /** + * Performs a search in all feeds or one specific feed. + */ + public static List performSearch(final Context context, + final String query, final long selectedFeed) { + final int values[] = {0, 0, 1, 2}; + final String[] subtitles = {context.getString(R.string.found_in_shownotes_label), + context.getString(R.string.found_in_shownotes_label), + context.getString(R.string.found_in_chapters_label), + context.getString(R.string.found_in_title_label)}; + + List result = new ArrayList(); + + FutureTask>[] tasks = new FutureTask[4]; + (tasks[0] = DBTasks.searchFeedItemContentEncoded(context, selectedFeed, query)).run(); + (tasks[1] = DBTasks.searchFeedItemDescription(context, selectedFeed, query)).run(); + (tasks[2] = DBTasks.searchFeedItemChapters(context, selectedFeed, query)).run(); + (tasks[3] = DBTasks.searchFeedItemTitle(context, selectedFeed, query)).run(); + try { + for (int i = 0; i < tasks.length; i++) { + FutureTask task = tasks[i]; + List items = (List) task.get(); + for (FeedItem item : items) { + result.add(new SearchResult(item, values[i], subtitles[i])); + } + + } + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + Collections.sort(result, new SearchResultValueComparator()); + return result; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/app/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java new file mode 100644 index 000000000..eb6592510 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -0,0 +1,1391 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.MergeCursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import java.util.Arrays; +import java.util.List; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedComponent; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; + +// TODO Remove media column from feeditem table + +/** + * Implements methods for accessing the database + */ +public class PodDBAdapter { + private static final String TAG = "PodDBAdapter"; + private static final int DATABASE_VERSION = 12; + public static final String DATABASE_NAME = "Antennapod.db"; + + /** + * Maximum number of arguments for IN-operator. + */ + public static final int IN_OPERATOR_MAXIMUM = 800; + + /** + * Maximum number of entries per search request. + */ + public static final int SEARCH_LIMIT = 30; + + // ----------- Column indices + // ----------- General indices + public static final int KEY_ID_INDEX = 0; + public static final int KEY_TITLE_INDEX = 1; + public static final int KEY_FILE_URL_INDEX = 2; + public static final int KEY_DOWNLOAD_URL_INDEX = 3; + public static final int KEY_DOWNLOADED_INDEX = 4; + public static final int KEY_LINK_INDEX = 5; + public static final int KEY_DESCRIPTION_INDEX = 6; + public static final int KEY_PAYMENT_LINK_INDEX = 7; + // ----------- Feed indices + public static final int KEY_LAST_UPDATE_INDEX = 8; + public static final int KEY_LANGUAGE_INDEX = 9; + public static final int KEY_AUTHOR_INDEX = 10; + public static final int KEY_IMAGE_INDEX = 11; + public static final int KEY_TYPE_INDEX = 12; + public static final int KEY_FEED_IDENTIFIER_INDEX = 13; + public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14; + public static final int KEY_FEED_USERNAME_INDEX = 15; + public static final int KEY_FEED_PASSWORD_INDEX = 16; + // ----------- FeedItem indices + public static final int KEY_CONTENT_ENCODED_INDEX = 2; + public static final int KEY_PUBDATE_INDEX = 3; + public static final int KEY_READ_INDEX = 4; + public static final int KEY_MEDIA_INDEX = 8; + public static final int KEY_FEED_INDEX = 9; + public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; + public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; + public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12; + // ---------- FeedMedia indices + public static final int KEY_DURATION_INDEX = 1; + public static final int KEY_POSITION_INDEX = 5; + public static final int KEY_SIZE_INDEX = 6; + public static final int KEY_MIME_TYPE_INDEX = 7; + public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; + public static final int KEY_MEDIA_FEEDITEM_INDEX = 9; + public static final int KEY_PLAYED_DURATION_INDEX = 10; + // --------- Download log indices + public static final int KEY_FEEDFILE_INDEX = 1; + public static final int KEY_FEEDFILETYPE_INDEX = 2; + public static final int KEY_REASON_INDEX = 3; + public static final int KEY_SUCCESSFUL_INDEX = 4; + public static final int KEY_COMPLETION_DATE_INDEX = 5; + public static final int KEY_REASON_DETAILED_INDEX = 6; + public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7; + // --------- Queue indices + public static final int KEY_FEEDITEM_INDEX = 1; + public static final int KEY_QUEUE_FEED_INDEX = 2; + // --------- Chapters indices + public static final int KEY_CHAPTER_START_INDEX = 2; + public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3; + public static final int KEY_CHAPTER_LINK_INDEX = 4; + public static final int KEY_CHAPTER_TYPE_INDEX = 5; + + // Key-constants + public static final String KEY_ID = "id"; + public static final String KEY_TITLE = "title"; + public static final String KEY_NAME = "name"; + public static final String KEY_LINK = "link"; + public static final String KEY_DESCRIPTION = "description"; + public static final String KEY_FILE_URL = "file_url"; + public static final String KEY_DOWNLOAD_URL = "download_url"; + public static final String KEY_PUBDATE = "pubDate"; + public static final String KEY_READ = "read"; + public static final String KEY_DURATION = "duration"; + public static final String KEY_POSITION = "position"; + public static final String KEY_SIZE = "filesize"; + public static final String KEY_MIME_TYPE = "mime_type"; + public static final String KEY_IMAGE = "image"; + public static final String KEY_FEED = "feed"; + public static final String KEY_MEDIA = "media"; + public static final String KEY_DOWNLOADED = "downloaded"; + public static final String KEY_LASTUPDATE = "last_update"; + public static final String KEY_FEEDFILE = "feedfile"; + public static final String KEY_REASON = "reason"; + public static final String KEY_SUCCESSFUL = "successful"; + public static final String KEY_FEEDFILETYPE = "feedfile_type"; + public static final String KEY_COMPLETION_DATE = "completion_date"; + public static final String KEY_FEEDITEM = "feeditem"; + public static final String KEY_CONTENT_ENCODED = "content_encoded"; + public static final String KEY_PAYMENT_LINK = "payment_link"; + public static final String KEY_START = "start"; + public static final String KEY_LANGUAGE = "language"; + public static final String KEY_AUTHOR = "author"; + public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; + public static final String KEY_TYPE = "type"; + public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; + public static final String KEY_FLATTR_STATUS = "flattr_status"; + public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; + public static final String KEY_REASON_DETAILED = "reason_detailed"; + public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; + public static final String KEY_CHAPTER_TYPE = "type"; + public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; + public static final String KEY_AUTO_DOWNLOAD = "auto_download"; + public static final String KEY_PLAYED_DURATION = "played_duration"; + public static final String KEY_USERNAME = "username"; + public static final String KEY_PASSWORD = "password"; + + // Table names + public static final String TABLE_NAME_FEEDS = "Feeds"; + public static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; + public static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; + public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; + public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; + public static final String TABLE_NAME_QUEUE = "Queue"; + public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; + + // SQL Statements for creating new tables + private static final String TABLE_PRIMARY_KEY = KEY_ID + + " INTEGER PRIMARY KEY AUTOINCREMENT ,"; + + private static final String CREATE_TABLE_FEEDS = "CREATE TABLE " + + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," + + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT," + + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR + + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," + + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," + + KEY_FLATTR_STATUS + " INTEGER," + + KEY_USERNAME + " TEXT," + + KEY_PASSWORD + " TEXT)"; + + private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE + + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," + + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + + KEY_FLATTR_STATUS + " INTEGER," + + KEY_IMAGE + " INTEGER)"; + + private static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE " + + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," + + KEY_DOWNLOADED + " INTEGER)"; + + private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " + + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION + + " INTEGER," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION + + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," + + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," + + KEY_FEEDITEM + " INTEGER," + + KEY_PLAYED_DURATION + " INTEGER)"; + + private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE + + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON + + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE + + " INTEGER," + KEY_REASON_DETAILED + " TEXT," + + KEY_DOWNLOADSTATUS_TITLE + " TEXT)"; + + private static final String CREATE_TABLE_QUEUE = "CREATE TABLE " + + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY," + + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)"; + + private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE " + + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER," + + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)"; + + private SQLiteDatabase db; + private final Context context; + private PodDBHelper helper; + + /** + * Select all columns from the feed-table + */ + private static final String[] FEED_SEL_STD = { + TABLE_NAME_FEEDS + "." + KEY_ID, + TABLE_NAME_FEEDS + "." + KEY_TITLE, + TABLE_NAME_FEEDS + "." + KEY_FILE_URL, + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL, + TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED, + TABLE_NAME_FEEDS + "." + KEY_LINK, + TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION, + TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK, + TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE, + TABLE_NAME_FEEDS + "." + KEY_LANGUAGE, + TABLE_NAME_FEEDS + "." + KEY_AUTHOR, + TABLE_NAME_FEEDS + "." + KEY_IMAGE, + TABLE_NAME_FEEDS + "." + KEY_TYPE, + TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, + TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, + TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS, + TABLE_NAME_FEEDS + "." + KEY_USERNAME, + TABLE_NAME_FEEDS + "." + KEY_PASSWORD + }; + + // column indices for FEED_SEL_STD + public static final int IDX_FEED_SEL_STD_ID = 0; + public static final int IDX_FEED_SEL_STD_TITLE = 1; + public static final int IDX_FEED_SEL_STD_FILE_URL = 2; + public static final int IDX_FEED_SEL_STD_DOWNLOAD_URL = 3; + public static final int IDX_FEED_SEL_STD_DOWNLOADED = 4; + public static final int IDX_FEED_SEL_STD_LINK = 5; + public static final int IDX_FEED_SEL_STD_DESCRIPTION = 6; + public static final int IDX_FEED_SEL_STD_PAYMENT_LINK = 7; + public static final int IDX_FEED_SEL_STD_LASTUPDATE = 8; + public static final int IDX_FEED_SEL_STD_LANGUAGE = 9; + public static final int IDX_FEED_SEL_STD_AUTHOR = 10; + public static final int IDX_FEED_SEL_STD_IMAGE = 11; + public static final int IDX_FEED_SEL_STD_TYPE = 12; + public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13; + public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14; + public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15; + public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 16; + public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 17; + + + /** + * Select all columns from the feeditems-table except description and + * content-encoded. + */ + private static final String[] FEEDITEM_SEL_FI_SMALL = { + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE, + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE, + TABLE_NAME_FEED_ITEMS + "." + KEY_READ, + TABLE_NAME_FEED_ITEMS + "." + KEY_LINK, + TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA, + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED, + TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER, + TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS, + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE}; + + /** + * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries. + */ + private static final String SEL_FI_SMALL_STR; + + static { + String selFiSmall = Arrays.toString(FEEDITEM_SEL_FI_SMALL); + SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1); + } + + // column indices for FEEDITEM_SEL_FI_SMALL + + public static final int IDX_FI_SMALL_ID = 0; + public static final int IDX_FI_SMALL_TITLE = 1; + public static final int IDX_FI_SMALL_PUBDATE = 2; + public static final int IDX_FI_SMALL_READ = 3; + public static final int IDX_FI_SMALL_LINK = 4; + public static final int IDX_FI_SMALL_PAYMENT_LINK = 5; + public static final int IDX_FI_SMALL_MEDIA = 6; + public static final int IDX_FI_SMALL_FEED = 7; + public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8; + public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9; + public static final int IDX_FI_SMALL_FLATTR_STATUS = 10; + public static final int IDX_FI_SMALL_IMAGE = 11; + + /** + * Select id, description and content-encoded column from feeditems. + */ + private static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION, + KEY_CONTENT_ENCODED, KEY_FEED}; + + // column indices for SEL_FI_EXTRA + + public static final int IDX_FI_EXTRA_ID = 0; + public static final int IDX_FI_EXTRA_DESCRIPTION = 1; + public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2; + public static final int IDX_FI_EXTRA_FEED = 3; + + static PodDBHelper dbHelperSingleton; + + private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) { + if (dbHelperSingleton == null) { + dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null, DATABASE_VERSION); + } + return dbHelperSingleton; + } + + public PodDBAdapter(Context c) { + this.context = c; + helper = getDbHelperSingleton(c.getApplicationContext()); + } + + public PodDBAdapter open() { + if (db == null || !db.isOpen() || db.isReadOnly()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Opening DB"); + try { + db = helper.getWritableDatabase(); + } catch (SQLException ex) { + ex.printStackTrace(); + db = helper.getReadableDatabase(); + } + } + return this; + } + + public void close() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Closing DB"); + //db.close(); + } + + public static boolean deleteDatabase(Context context) { + Log.w(TAG, "Deleting database"); + dbHelperSingleton.close(); + dbHelperSingleton = null; + return context.deleteDatabase(DATABASE_NAME); + } + + /** + * Inserts or updates a feed entry + * + * @return the id of the entry + */ + public long setFeed(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, feed.getTitle()); + values.put(KEY_LINK, feed.getLink()); + values.put(KEY_DESCRIPTION, feed.getDescription()); + values.put(KEY_PAYMENT_LINK, feed.getPaymentLink()); + values.put(KEY_AUTHOR, feed.getAuthor()); + values.put(KEY_LANGUAGE, feed.getLanguage()); + if (feed.getImage() != null) { + if (feed.getImage().getId() == 0) { + setImage(feed.getImage()); + } + values.put(KEY_IMAGE, feed.getImage().getId()); + } + + values.put(KEY_FILE_URL, feed.getFile_url()); + values.put(KEY_DOWNLOAD_URL, feed.getDownload_url()); + values.put(KEY_DOWNLOADED, feed.isDownloaded()); + values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime()); + values.put(KEY_TYPE, feed.getType()); + values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); + + Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong()); + + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + if (feed.getId() == 0) { + // Create new entry + if (BuildConfig.DEBUG) + Log.d(this.toString(), "Inserting new Feed into db"); + feed.setId(db.insert(TABLE_NAME_FEEDS, null, values)); + } else { + if (BuildConfig.DEBUG) + Log.d(this.toString(), "Updating existing Feed in db"); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", + new String[]{String.valueOf(feed.getId())}); + + } + return feed.getId(); + } + + public void setFeedPreferences(FeedPreferences prefs) { + if (prefs.getFeedID() == 0) { + throw new IllegalArgumentException("Feed ID of preference must not be null"); + } + ContentValues values = new ContentValues(); + values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload()); + values.put(KEY_USERNAME, prefs.getUsername()); + values.put(KEY_PASSWORD, prefs.getPassword()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); + } + + /** + * Inserts or updates an image entry + * + * @return the id of the entry + */ + public long setImage(FeedImage image) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, image.getTitle()); + values.put(KEY_DOWNLOAD_URL, image.getDownload_url()); + values.put(KEY_DOWNLOADED, image.isDownloaded()); + values.put(KEY_FILE_URL, image.getFile_url()); + if (image.getId() == 0) { + image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values)); + } else { + db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?", + new String[]{String.valueOf(image.getId())}); + } + + final FeedComponent owner = image.getOwner(); + if (owner != null && owner.getId() != 0) { + values.clear(); + values.put(KEY_IMAGE, image.getId()); + if (owner instanceof Feed) { + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())}); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + return image.getId(); + } + + /** + * Inserts or updates an image entry + * + * @return the id of the entry + */ + public long setMedia(FeedMedia media) { + ContentValues values = new ContentValues(); + values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_POSITION, media.getPosition()); + values.put(KEY_SIZE, media.getSize()); + values.put(KEY_MIME_TYPE, media.getMime_type()); + values.put(KEY_DOWNLOAD_URL, media.getDownload_url()); + values.put(KEY_DOWNLOADED, media.isDownloaded()); + values.put(KEY_FILE_URL, media.getFile_url()); + + if (media.getPlaybackCompletionDate() != null) { + values.put(KEY_PLAYBACK_COMPLETION_DATE, media + .getPlaybackCompletionDate().getTime()); + } else { + values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); + } + if (media.getItem() != null) { + values.put(KEY_FEEDITEM, media.getItem().getId()); + } + if (media.getId() == 0) { + media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values)); + } else { + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } + return media.getId(); + } + + public void setFeedMediaPlaybackInformation(FeedMedia media) { + if (media.getId() != 0) { + ContentValues values = new ContentValues(); + values.put(KEY_POSITION, media.getPosition()); + values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } else { + Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0"); + } + } + + public void setFeedMediaPlaybackCompletionDate(FeedMedia media) { + if (media.getId() != 0) { + ContentValues values = new ContentValues(); + values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } else { + Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0"); + } + } + + /** + * Insert all FeedItems of a feed and the feed object itself in a single + * transaction + */ + public void setCompleteFeed(Feed... feeds) { + db.beginTransaction(); + for (Feed feed : feeds) { + setFeed(feed); + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + setFeedItem(item, false); + } + } + if (feed.getPreferences() != null) { + setFeedPreferences(feed.getPreferences()); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + /** + * Update the flattr status of a feed + */ + public void setFeedFlattrStatus(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); + } + + /** + * Get all feeds in the flattr queue. + */ + public Cursor getFeedsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); + } + + /** + * Get all feed items in the flattr queue. + */ + public Cursor getFeedItemsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); + } + + /** + * Counts feeds and feed items in the flattr queue + */ + public int getFlattrQueueSize() { + int res = 0; + Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res = c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds"); + } + c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res += c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items"); + } + + return res; + } + + /** + * Updates the download URL of a Feed. + */ + public void setFeedDownloadUrl(String original, String updated) { + ContentValues values = new ContentValues(); + values.put(KEY_DOWNLOAD_URL, updated); + db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original}); + } + + public void setFeedItemlist(List items) { + db.beginTransaction(); + for (FeedItem item : items) { + setFeedItem(item, true); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public long setSingleFeedItem(FeedItem item) { + db.beginTransaction(); + long result = setFeedItem(item, true); + db.setTransactionSuccessful(); + db.endTransaction(); + return result; + } + + /** + * Update the flattr status of a FeedItem + */ + public void setFeedItemFlattrStatus(FeedItem feedItem) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feedItem.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(feedItem.getId())}); + } + + /** + * Update the flattr status of a feed or feed item specified by its payment link + * and the new flattr status to use + */ + public void setItemFlattrStatus(String url, FlattrStatus status) { + //Log.d(TAG, "setItemFlattrStatus(" + url + ") = " + status.toString()); + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, status.toLong()); + + // regexps in sqlite would be neat! + String[] query_urls = new String[]{ + "*" + url + "&*", + "*" + url + "%2F&*", + "*" + url + "", + "*" + url + "%2F" + }; + + if (db.update(TABLE_NAME_FEEDS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls + ) > 0) { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in Feeds table"); + return; + } + if (db.update(TABLE_NAME_FEED_ITEMS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls + ) > 0) { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in FeedsItems table"); + } + } + + /** + * Reset flattr status to unflattrd for all items + */ + public void clearAllFlattrStatus() { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, 0); + db.update(TABLE_NAME_FEEDS, values, null, null); + db.update(TABLE_NAME_FEED_ITEMS, values, null, null); + } + + /** + * Inserts or updates a feeditem entry + * + * @param item The FeedItem + * @param saveFeed true if the Feed of the item should also be saved. This should be set to + * false if the method is executed on a list of FeedItems of the same Feed. + * @return the id of the entry + */ + private long setFeedItem(FeedItem item, boolean saveFeed) { + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, item.getTitle()); + values.put(KEY_LINK, item.getLink()); + if (item.getDescription() != null) { + values.put(KEY_DESCRIPTION, item.getDescription()); + } + if (item.getContentEncoded() != null) { + values.put(KEY_CONTENT_ENCODED, item.getContentEncoded()); + } + values.put(KEY_PUBDATE, item.getPubDate().getTime()); + values.put(KEY_PAYMENT_LINK, item.getPaymentLink()); + if (saveFeed && item.getFeed() != null) { + setFeed(item.getFeed()); + } + values.put(KEY_FEED, item.getFeed().getId()); + values.put(KEY_READ, item.isRead()); + values.put(KEY_HAS_CHAPTERS, item.getChapters() != null); + values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); + values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong()); + if (item.hasItemImage()) { + if (item.getImage().getId() == 0) { + setImage(item.getImage()); + } + values.put(KEY_IMAGE, item.getImage().getId()); + } + + if (item.getId() == 0) { + item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); + } else { + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", + new String[]{String.valueOf(item.getId())}); + } + if (item.getMedia() != null) { + setMedia(item.getMedia()); + } + if (item.getChapters() != null) { + setChapters(item); + } + return item.getId(); + } + + public void setFeedItemRead(boolean read, long itemId, long mediaId, + boolean resetMediaPosition) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + + values.put(KEY_READ, read); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)}); + + if (resetMediaPosition) { + values.clear(); + values.put(KEY_POSITION, 0); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + } + + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void setFeedItemRead(boolean read, long... itemIds) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + for (long id : itemIds) { + values.clear(); + values.put(KEY_READ, read); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(id)}); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void setChapters(FeedItem item) { + ContentValues values = new ContentValues(); + for (Chapter chapter : item.getChapters()) { + values.put(KEY_TITLE, chapter.getTitle()); + values.put(KEY_START, chapter.getStart()); + values.put(KEY_FEEDITEM, item.getId()); + values.put(KEY_LINK, chapter.getLink()); + values.put(KEY_CHAPTER_TYPE, chapter.getChapterType()); + if (chapter.getId() == 0) { + chapter.setId(db + .insert(TABLE_NAME_SIMPLECHAPTERS, null, values)); + } else { + db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?", + new String[]{String.valueOf(chapter.getId())}); + } + } + } + + /** + * Inserts or updates a download status. + */ + public long setDownloadStatus(DownloadStatus status) { + ContentValues values = new ContentValues(); + values.put(KEY_FEEDFILE, status.getFeedfileId()); + values.put(KEY_FEEDFILETYPE, status.getFeedfileType()); + values.put(KEY_REASON, status.getReason().getCode()); + values.put(KEY_SUCCESSFUL, status.isSuccessful()); + values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime()); + values.put(KEY_REASON_DETAILED, status.getReasonDetailed()); + values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle()); + if (status.getId() == 0) { + status.setId(db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values)); + } else { + db.update(TABLE_NAME_DOWNLOAD_LOG, values, KEY_ID + "=?", + new String[]{String.valueOf(status.getId())}); + } + return status.getId(); + } + + public long getDownloadLogSize() { + final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_DOWNLOAD_LOG); + Cursor result = db.rawQuery(query, null); + long count = 0; + if (result.moveToFirst()) { + count = result.getLong(0); + } + result.close(); + return count; + } + + public void removeDownloadLogItems(long count) { + if (count > 0) { + final String sql = String.format("DELETE FROM %s WHERE %s in (SELECT %s from %s ORDER BY %s ASC LIMIT %d)", + TABLE_NAME_DOWNLOAD_LOG, KEY_ID, KEY_ID, TABLE_NAME_DOWNLOAD_LOG, KEY_COMPLETION_DATE, count); + db.execSQL(sql, null); + } + } + + public void setQueue(List queue) { + ContentValues values = new ContentValues(); + db.beginTransaction(); + db.delete(TABLE_NAME_QUEUE, null, null); + for (int i = 0; i < queue.size(); i++) { + FeedItem item = queue.get(i); + values.put(KEY_ID, i); + values.put(KEY_FEEDITEM, item.getId()); + values.put(KEY_FEED, item.getFeed().getId()); + db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void clearQueue() { + db.delete(TABLE_NAME_QUEUE, null, null); + } + + public void removeFeedMedia(FeedMedia media) { + db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } + + public void removeChaptersOfItem(FeedItem item) { + db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?", + new String[]{String.valueOf(item.getId())}); + } + + public void removeFeedImage(FeedImage image) { + db.delete(TABLE_NAME_FEED_IMAGES, KEY_ID + "=?", + new String[]{String.valueOf(image.getId())}); + } + + /** + * Remove a FeedItem and its FeedMedia entry. + */ + public void removeFeedItem(FeedItem item) { + if (item.getMedia() != null) { + removeFeedMedia(item.getMedia()); + } + if (item.getChapters() != null) { + removeChaptersOfItem(item); + } + if (item.hasItemImage()) { + removeFeedImage(item.getImage()); + } + db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?", + new String[]{String.valueOf(item.getId())}); + } + + /** + * Remove a feed with all its FeedItems and Media entries. + */ + public void removeFeed(Feed feed) { + db.beginTransaction(); + if (feed.getImage() != null) { + removeFeedImage(feed.getImage()); + } + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + removeFeedItem(item); + } + } + + db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?", + new String[]{String.valueOf(feed.getId())}); + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void removeDownloadStatus(DownloadStatus remove) { + db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?", + new String[]{String.valueOf(remove.getId())}); + } + + public void clearPlaybackHistory() { + ContentValues values = new ContentValues(); + values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); + db.update(TABLE_NAME_FEED_MEDIA, values, null, null); + } + + /** + * Get all Feeds from the Feed Table. + * + * @return The cursor of the query + */ + public final Cursor getAllFeedsCursor() { + Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null, + KEY_TITLE + " COLLATE NOCASE ASC"); + return c; + } + + public final Cursor getFeedCursorDownloadUrls() { + return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null); + } + + public final Cursor getExpiredFeedsCursor(long expirationTime) { + Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_LASTUPDATE + " < " + String.valueOf(System.currentTimeMillis() - expirationTime), + null, null, null, + null); + return c; + } + + /** + * Returns a cursor with all FeedItems of a Feed. Uses FEEDITEM_SEL_FI_SMALL + * + * @param feed The feed you want to get the FeedItems from. + * @return The cursor of the query + */ + public final Cursor getAllItemsOfFeedCursor(final Feed feed) { + return getAllItemsOfFeedCursor(feed.getId()); + } + + public final Cursor getAllItemsOfFeedCursor(final long feedId) { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=?", new String[]{String.valueOf(feedId)}, null, null, + null + ); + return c; + } + + /** + * Return a cursor with the SEL_FI_EXTRA selection of a single feeditem. + */ + public final Cursor getExtraInformationOfItem(final FeedItem item) { + Cursor c = db + .query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_ID + "=?", + new String[]{String.valueOf(item.getId())}, null, + null, null); + return c; + } + + /** + * Returns a cursor for a DB query in the FeedMedia table for a given ID. + * + * @param item The item you want to get the FeedMedia from + * @return The cursor of the query + */ + public final Cursor getFeedMediaOfItemCursor(final FeedItem item) { + Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", + new String[]{String.valueOf(item.getMedia().getId())}, null, + null, null); + return c; + } + + /** + * Returns a cursor for a DB query in the FeedImages table for a given ID. + * + * @param id ID of the FeedImage + * @return The cursor of the query + */ + public final Cursor getImageCursor(final long id) { + Cursor c = db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + "=?", + new String[]{String.valueOf(id)}, null, null, null); + return c; + } + + public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) { + Cursor c = db.query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_FEEDITEM + + "=?", new String[]{String.valueOf(item.getId())}, null, + null, null + ); + return c; + } + + public final Cursor getDownloadLogCursor(final int limit) { + Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null, + null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit); + return c; + } + + /** + * Returns a cursor which contains all feed items in the queue. The returned + * cursor uses the FEEDITEM_SEL_FI_SMALL selection. + */ + public final Cursor getQueueCursor() { + Object[] args = (Object[]) new String[]{ + SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE, + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM, + TABLE_NAME_QUEUE + "." + KEY_ID}; + String query = String.format( + "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args); + Cursor c = db.rawQuery(query, null); + /* + * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, + * "INNER JOIN ? ON ?=?", new String[] { TABLE_NAME_QUEUE, + * TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." + + * KEY_FEEDITEM }, null, null, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM); + */ + return c; + } + + public Cursor getQueueIDCursor() { + Cursor c = db.query(TABLE_NAME_QUEUE, new String[]{KEY_FEEDITEM}, null, null, null, null, KEY_ID + " ASC", null); + return c; + } + + /** + * Returns a cursor which contains all feed items in the unread items list. + * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. + */ + public final Cursor getUnreadItemsCursor() { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_READ + + "=0", null, null, null, KEY_PUBDATE + " DESC"); + return c; + } + + public final Cursor getUnreadItemIdsCursor() { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID}, + KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC"); + return c; + + } + + public final Cursor getRecentlyPublishedItemsCursor(int limit) { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, null, null, null, null, KEY_PUBDATE + " DESC LIMIT " + limit); + return c; + } + + public Cursor getDownloadedItemsCursor() { + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE " + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0"; + Cursor c = db.rawQuery(query, null); + return c; + } + + /** + * Returns a cursor which contains feed media objects with a playback + * completion date in ascending order. + * + * @param limit The maximum row count of the returned cursor. Must be an + * integer >= 0. + * @throws IllegalArgumentException if limit < 0 + */ + public final Cursor getCompletedMediaCursor(int limit) { + Validate.isTrue(limit >= 0, "Limit must be >= 0"); + + Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, + KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null, + null, null); + return c; + } + + public final Cursor getSingleFeedMediaCursor(long id) { + return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null); + } + + public final Cursor getFeedMediaCursorByItemID(String... mediaIds) { + int length = mediaIds.length; + if (length > IN_OPERATOR_MAXIMUM) { + Log.w(TAG, "Length of id array is larger than " + + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors"); + int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1; + Cursor[] cursors = new Cursor[numCursors]; + for (int i = 0; i < numCursors; i++) { + int neededLength = 0; + String[] parts = null; + final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM; + + if (elementsLeft >= IN_OPERATOR_MAXIMUM) { + neededLength = IN_OPERATOR_MAXIMUM; + parts = Arrays.copyOfRange(mediaIds, i + * IN_OPERATOR_MAXIMUM, (i + 1) + * IN_OPERATOR_MAXIMUM); + } else { + neededLength = elementsLeft; + parts = Arrays.copyOfRange(mediaIds, i + * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM) + + neededLength); + } + + cursors[i] = db.rawQuery("SELECT * FROM " + + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_FEEDITEM + " IN " + + buildInOperator(neededLength), parts); + } + return new MergeCursor(cursors); + } else { + return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN " + + buildInOperator(length), mediaIds, null, null, null); + } + } + + /** + * Builds an IN-operator argument depending on the number of items. + */ + private String buildInOperator(int size) { + if (size == 1) { + return "(?)"; + } + StringBuffer buffer = new StringBuffer("("); + for (int i = 0; i < size - 1; i++) { + buffer.append("?,"); + } + buffer.append("?)"); + return buffer.toString(); + } + + public final Cursor getFeedCursor(final long id) { + Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_ID + "=" + id, null, + null, null, null); + return c; + } + + public final Cursor getFeedItemCursor(final String... ids) { + if (ids.length > IN_OPERATOR_MAXIMUM) { + throw new IllegalArgumentException( + "number of IDs must not be larger than " + + IN_OPERATOR_MAXIMUM + ); + } + + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_ID + " IN " + + buildInOperator(ids.length), ids, null, null, null); + + } + + public int getQueueSize() { + final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE); + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + public final int getNumberOfUnreadItems() { + final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS + + " WHERE " + KEY_READ + " = 0"; + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + public final int getNumberOfDownloadedEpisodes() { + final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + + " WHERE " + KEY_DOWNLOADED + " > 0"; + + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + /** + * Uses DatabaseUtils to escape a search query and removes ' at the + * beginning and the end of the string returned by the escape method. + */ + private String prepareSearchQuery(String query) { + StringBuilder builder = new StringBuilder(); + DatabaseUtils.appendEscapedSQLString(builder, query); + builder.deleteCharAt(0); + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + /** + * Searches for the given query in the description of all items or the items + * of a specified feed. + * + * @return A cursor with all search results in SEL_FI_EXTRA selection. + */ + public Cursor searchItemDescriptions(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_DESCRIPTION + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null + ); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, + KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query) + + "%'", null, null, null, null + ); + } + } + + /** + * Searches for the given query in the content-encoded field of all items or + * the items of a specified feed. + * + * @return A cursor with all search results in SEL_FI_EXTRA selection. + */ + public Cursor searchItemContentEncoded(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null + ); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, + KEY_CONTENT_ENCODED + " LIKE '%" + + prepareSearchQuery(query) + "%'", null, null, + null, null + ); + } + } + + public Cursor searchItemTitles(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null + ); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(query) + "%'", null, null, + null, null + ); + } + } + + public Cursor searchItemChapters(long feedID, String searchQuery) { + final String query; + if (feedID != 0) { + query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + + TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + + feedID + " AND " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(searchQuery) + "%'"; + } else { + query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + + TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(searchQuery) + "%'"; + } + return db.rawQuery(query, null); + } + + + public static final int IDX_FEEDSTATISTICS_FEED = 0; + public static final int IDX_FEEDSTATISTICS_NUM_ITEMS = 1; + public static final int IDX_FEEDSTATISTICS_NEW_ITEMS = 2; + public static final int IDX_FEEDSTATISTICS_LATEST_EPISODE = 3; + public static final int IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES = 4; + + /** + * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result + * is sorted by the title of the feed. + */ + private static final String FEED_STATISTICS_QUERY = "SELECT Feeds.id, num_items, new_items, latest_episode, in_progress FROM " + + " Feeds LEFT JOIN " + + "(SELECT feed,count(*) AS num_items," + + " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," + + " MAX(pubDate) AS latest_episode," + + " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + + " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + + " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + + " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;"; + + public Cursor getFeedStatisticsCursor() { + return db.rawQuery(FEED_STATISTICS_QUERY, null); + } + + /** + * Helper class for opening the Antennapod database. + */ + private static class PodDBHelper extends SQLiteOpenHelper { + /** + * Constructor. + * + * @param context Context to use + * @param name Name of the database + * @param factory to use for creating cursor objects + * @param version number of the database + */ + public PodDBHelper(final Context context, final String name, + final CursorFactory factory, final int version) { + super(context, name, factory, version); + } + + @Override + public void onCreate(final SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_FEEDS); + db.execSQL(CREATE_TABLE_FEED_ITEMS); + db.execSQL(CREATE_TABLE_FEED_IMAGES); + db.execSQL(CREATE_TABLE_FEED_MEDIA); + db.execSQL(CREATE_TABLE_DOWNLOAD_LOG); + db.execSQL(CREATE_TABLE_QUEUE); + db.execSQL(CREATE_TABLE_SIMPLECHAPTERS); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, + final int newVersion) { // TODO onUpgrade + Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + + newVersion + "."); + if (oldVersion <= 1) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " + + KEY_TYPE + " TEXT"); + } + if (oldVersion <= 2) { + db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + KEY_LINK + " TEXT"); + } + if (oldVersion <= 3) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_ITEM_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 4) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " + + KEY_FEED_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 5) { + db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + KEY_REASON_DETAILED + " TEXT"); + db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + KEY_DOWNLOADSTATUS_TITLE + " TEXT"); + } + if (oldVersion <= 6) { + db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + KEY_CHAPTER_TYPE + " INTEGER"); + } + if (oldVersion <= 7) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_PLAYBACK_COMPLETION_DATE + + " INTEGER"); + } + if (oldVersion <= 8) { + final int KEY_ID_POSITION = 0; + final int KEY_MEDIA_POSITION = 1; + + // Add feeditem column to feedmedia table + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_FEEDITEM + + " INTEGER"); + Cursor feeditemCursor = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID, KEY_MEDIA}, "? > 0", new String[]{KEY_MEDIA}, null, null, null); + if (feeditemCursor.moveToFirst()) { + db.beginTransaction(); + ContentValues contentValues = new ContentValues(); + do { + long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); + contentValues.put(KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); + db.update(TABLE_NAME_FEED_MEDIA, contentValues, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + contentValues.clear(); + } while (feeditemCursor.moveToNext()); + db.setTransactionSuccessful(); + db.endTransaction(); + } + feeditemCursor.close(); + } + if (oldVersion <= 9) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_AUTO_DOWNLOAD + + " INTEGER DEFAULT 1"); + } + if (oldVersion <= 10) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_PLAYED_DURATION + + " INTEGER"); + } + if (oldVersion <= 11) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_USERNAME + + " TEXT"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_PASSWORD + + " TEXT"); + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_IMAGE + + " INTEGER"); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java new file mode 100644 index 000000000..9efc5888f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.feed.Feed; +import org.apache.commons.io.input.XmlStreamReader; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.File; +import java.io.IOException; +import java.io.Reader; + +public class FeedHandler { + + public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException, + ParserConfigurationException, UnsupportedFeedtypeException { + TypeGetter tg = new TypeGetter(); + TypeGetter.Type type = tg.getType(feed); + SyndHandler handler = new SyndHandler(feed, type); + + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + SAXParser saxParser = factory.newSAXParser(); + File file = new File(feed.getFile_url()); + Reader inputStreamReader = new XmlStreamReader(file); + InputSource inputSource = new InputSource(inputStreamReader); + + saxParser.parse(inputSource, handler); + inputStreamReader.close(); + return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java new file mode 100644 index 000000000..45d1413bf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.feed.Feed; + +import java.util.Map; + +/** + * Container for results returned by the Feed parser + */ +public class FeedHandlerResult { + + public Feed feed; + public Map alternateFeedUrls; + + public FeedHandlerResult(Feed feed, Map alternateFeedUrls) { + this.feed = feed; + this.alternateFeedUrls = alternateFeedUrls; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java new file mode 100644 index 000000000..4fe8e1aff --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java @@ -0,0 +1,98 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; + +import java.util.*; + +/** + * Contains all relevant information to describe the current state of a + * SyndHandler. + */ +public class HandlerState { + + /** + * Feed that the Handler is currently processing. + */ + protected Feed feed; + /** + * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the + * URL of the feed, the value is the title + */ + protected Map alternateUrls; + protected ArrayList items; + protected FeedItem currentItem; + protected Stack tagstack; + /** + * Namespaces that have been defined so far. + */ + protected HashMap namespaces; + protected Stack defaultNamespaces; + /** + * Buffer for saving characters. + */ + protected StringBuffer contentBuf; + + public HandlerState(Feed feed) { + this.feed = feed; + alternateUrls = new LinkedHashMap(); + items = new ArrayList(); + tagstack = new Stack(); + namespaces = new HashMap(); + defaultNamespaces = new Stack(); + } + + public Feed getFeed() { + return feed; + } + + public ArrayList getItems() { + return items; + } + + public FeedItem getCurrentItem() { + return currentItem; + } + + public Stack getTagstack() { + return tagstack; + } + + public void setFeed(Feed feed) { + this.feed = feed; + } + + public void setCurrentItem(FeedItem currentItem) { + this.currentItem = currentItem; + } + + /** + * Returns the SyndElement that comes after the top element of the tagstack. + */ + public SyndElement getSecondTag() { + SyndElement top = tagstack.pop(); + SyndElement second = tagstack.peek(); + tagstack.push(top); + return second; + } + + public SyndElement getThirdTag() { + SyndElement top = tagstack.pop(); + SyndElement second = tagstack.pop(); + SyndElement third = tagstack.peek(); + tagstack.push(second); + tagstack.push(top); + return third; + } + + public StringBuffer getContentBuf() { + return contentBuf; + } + + public void addAlternateFeedUrl(String title, String url) { + alternateUrls.put(url, title); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java new file mode 100644 index 000000000..573c873eb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java @@ -0,0 +1,126 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.syndication.namespace.*; +import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** Superclass for all SAX Handlers which process Syndication formats */ +public class SyndHandler extends DefaultHandler { + private static final String TAG = "SyndHandler"; + private static final String DEFAULT_PREFIX = ""; + protected HandlerState state; + + public SyndHandler(Feed feed, TypeGetter.Type type) { + state = new HandlerState(feed); + if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) { + state.defaultNamespaces.push(new NSRSS20()); + } + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + state.contentBuf = new StringBuffer(); + Namespace handler = getHandlingNamespace(uri, qName); + if (handler != null) { + SyndElement element = handler.handleElementStart(localName, state, + attributes); + state.tagstack.push(element); + + } + } + + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if (!state.tagstack.empty()) { + if (state.getTagstack().size() >= 2) { + if (state.contentBuf != null) { + state.contentBuf.append(ch, start, length); + } + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + Namespace handler = getHandlingNamespace(uri, qName); + if (handler != null) { + handler.handleElementEnd(localName, state); + state.tagstack.pop(); + + } + state.contentBuf = null; + + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.pop(); + } + } + + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + // Find the right namespace + if (!state.namespaces.containsKey(uri)) { + if (uri.equals(NSAtom.NSURI)) { + if (prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.push(new NSAtom()); + } else if (prefix.equals(NSAtom.NSTAG)) { + state.namespaces.put(uri, new NSAtom()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized Atom namespace"); + } + } else if (uri.equals(NSContent.NSURI) + && prefix.equals(NSContent.NSTAG)) { + state.namespaces.put(uri, new NSContent()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized Content namespace"); + } else if (uri.equals(NSITunes.NSURI) + && prefix.equals(NSITunes.NSTAG)) { + state.namespaces.put(uri, new NSITunes()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized ITunes namespace"); + } else if (uri.equals(NSSimpleChapters.NSURI) + && prefix.matches(NSSimpleChapters.NSTAG)) { + state.namespaces.put(uri, new NSSimpleChapters()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized SimpleChapters namespace"); + } else if (uri.equals(NSMedia.NSURI) + && prefix.equals(NSMedia.NSTAG)) { + state.namespaces.put(uri, new NSMedia()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized media namespace"); + } + } + } + + private Namespace getHandlingNamespace(String uri, String qName) { + Namespace handler = state.namespaces.get(uri); + if (handler == null && !state.defaultNamespaces.empty() + && !qName.contains(":")) { + handler = state.defaultNamespaces.peek(); + } + return handler; + } + + @Override + public void endDocument() throws SAXException { + super.endDocument(); + state.getFeed().setItems(state.getItems()); + } + + public HandlerState getState() { + return state; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java new file mode 100644 index 000000000..e1ebd63a5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java @@ -0,0 +1,111 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import org.apache.commons.io.input.XmlStreamReader; +import org.jsoup.Jsoup; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; + +/** Gets the type of a specific feed by reading the root element. */ +public class TypeGetter { + private static final String TAG = "TypeGetter"; + + public enum Type { + RSS20, RSS091, ATOM, INVALID + } + + private static final String ATOM_ROOT = "feed"; + private static final String RSS_ROOT = "rss"; + + public Type getType(Feed feed) throws UnsupportedFeedtypeException { + XmlPullParserFactory factory; + if (feed.getFile_url() != null) { + try { + factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(createReader(feed)); + int eventType = xpp.getEventType(); + + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + String tag = xpp.getName(); + if (tag.equals(ATOM_ROOT)) { + feed.setType(Feed.TYPE_ATOM1); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized type Atom"); + return Type.ATOM; + } else if (tag.equals(RSS_ROOT)) { + String strVersion = xpp.getAttributeValue(null, + "version"); + if (strVersion != null) { + + if (strVersion.equals("2.0")) { + feed.setType(Feed.TYPE_RSS2); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized type RSS 2.0"); + return Type.RSS20; + } else if (strVersion.equals("0.91") + || strVersion.equals("0.92")) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Recognized type RSS 0.91/0.92"); + return Type.RSS091; + } + } + throw new UnsupportedFeedtypeException(Type.INVALID); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Type is invalid"); + throw new UnsupportedFeedtypeException(Type.INVALID, tag); + } + } else { + eventType = xpp.next(); + } + } + + } catch (XmlPullParserException e) { + e.printStackTrace(); + // XML document might actually be a HTML document -> try to parse as HTML + String rootElement = null; + try { + if (Jsoup.parse(new File(feed.getFile_url()), null) != null) { + rootElement = "html"; + } + } catch (IOException e1) { + e1.printStackTrace(); + } finally { + throw new UnsupportedFeedtypeException(Type.INVALID, rootElement); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Type is invalid"); + throw new UnsupportedFeedtypeException(Type.INVALID); + } + + private Reader createReader(Feed feed) { + Reader reader; + try { + reader = new XmlStreamReader(new File(feed.getFile_url())); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return reader; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java new file mode 100644 index 000000000..3da9251d9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.syndication.handler.TypeGetter.Type; + +public class UnsupportedFeedtypeException extends Exception { + private static final long serialVersionUID = 9105878964928170669L; + private TypeGetter.Type type; + private String rootElement; + + public UnsupportedFeedtypeException(Type type) { + super(); + this.type = type; + } + + public UnsupportedFeedtypeException(Type type, String rootElement) { + this.type = type; + this.rootElement = rootElement; + } + + public TypeGetter.Type getType() { + return type; + } + + public String getRootElement() { + return rootElement; + } + + @Override + public String getMessage() { + if (type == TypeGetter.Type.INVALID) { + return "Invalid type"; + } else { + return "Type " + type + " not supported"; + } + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java new file mode 100644 index 000000000..71bf69ffa --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + +public class NSContent extends Namespace { + public static final String NSTAG = "content"; + public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; + + private static final String ENCODED = "encoded"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(ENCODED)) { + state.getCurrentItem().setContentEncoded(state.getContentBuf().toString()); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java new file mode 100644 index 000000000..fb794d7e0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java @@ -0,0 +1,51 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + +public class NSITunes extends Namespace { + public static final String NSTAG = "itunes"; + public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd"; + + private static final String IMAGE = "image"; + private static final String IMAGE_TITLE = "image"; + private static final String IMAGE_HREF = "href"; + + private static final String AUTHOR = "author"; + + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(IMAGE)) { + FeedImage image = new FeedImage(); + image.setTitle(IMAGE_TITLE); + image.setDownload_url(attributes.getValue(IMAGE_HREF)); + + if (state.getCurrentItem() != null) { + // this is an items image + image.setTitle(state.getCurrentItem().getTitle() + IMAGE_TITLE); + state.getCurrentItem().setImage(image); + + } else { + // this is the feed image + if (state.getFeed().getImage() == null) { + state.getFeed().setImage(image); + } + } + + } + + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(AUTHOR)) { + state.getFeed().setAuthor(state.getContentBuf().toString()); + } + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java new file mode 100644 index 000000000..15c377f79 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import org.xml.sax.Attributes; + +import java.util.concurrent.TimeUnit; + +/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ +public class NSMedia extends Namespace { + private static final String TAG = "NSMedia"; + + public static final String NSTAG = "media"; + public static final String NSURI = "http://search.yahoo.com/mrss/"; + + private static final String CONTENT = "content"; + private static final String DOWNLOAD_URL = "url"; + private static final String SIZE = "fileSize"; + private static final String MIME_TYPE = "type"; + private static final String DURATION = "duration"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(CONTENT)) { + String url = attributes.getValue(DOWNLOAD_URL); + String type = attributes.getValue(MIME_TYPE); + if (state.getCurrentItem().getMedia() == null + && url != null + && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils + .getValidMimeTypeFromUrl(url)) != null))) { + + long size = 0; + try { + size = Long.parseLong(attributes.getValue(SIZE)); + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length attribute could not be parsed."); + } + + int duration = 0; + try { + String durationStr = attributes.getValue(DURATION); + if (durationStr != null) { + duration = (int) TimeUnit.MILLISECONDS.convert( + Long.parseLong(durationStr), TimeUnit.SECONDS); + } + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Duration attribute could not be parsed"); + } + + state.getCurrentItem().setMedia( + new FeedMedia(state.getCurrentItem(), url, size, type)); + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java new file mode 100644 index 000000000..fd8f6176b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java @@ -0,0 +1,141 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import org.xml.sax.Attributes; + +/** + * SAX-Parser for reading RSS-Feeds + * + * @author daniel + * + */ +public class NSRSS20 extends Namespace { + private static final String TAG = "NSRSS20"; + public static final String NSTAG = "rss"; + public static final String NSURI = ""; + + public final static String CHANNEL = "channel"; + public final static String ITEM = "item"; + public final static String GUID = "guid"; + public final static String TITLE = "title"; + public final static String LINK = "link"; + public final static String DESCR = "description"; + public final static String PUBDATE = "pubDate"; + public final static String ENCLOSURE = "enclosure"; + public final static String IMAGE = "image"; + public final static String URL = "url"; + public final static String LANGUAGE = "language"; + + public final static String ENC_URL = "url"; + public final static String ENC_LEN = "length"; + public final static String ENC_TYPE = "type"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(ITEM)) { + state.setCurrentItem(new FeedItem()); + state.getItems().add(state.getCurrentItem()); + state.getCurrentItem().setFeed(state.getFeed()); + + } else if (localName.equals(ENCLOSURE)) { + String type = attributes.getValue(ENC_TYPE); + String url = attributes.getValue(ENC_URL); + if (state.getCurrentItem().getMedia() == null + && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils + .getValidMimeTypeFromUrl(url)) != null))) { + + long size = 0; + try { + size = Long.parseLong(attributes.getValue(ENC_LEN)); + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length attribute could not be parsed."); + } + state.getCurrentItem().setMedia( + new FeedMedia(state.getCurrentItem(), url, size, type)); + } + + } else if (localName.equals(IMAGE)) { + if (state.getTagstack().size() >= 1) { + String parent = state.getTagstack().peek().getName(); + if (parent.equals(CHANNEL)) { + state.getFeed().setImage(new FeedImage()); + } + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(ITEM)) { + if (state.getCurrentItem() != null) { + // the title tag is optional in RSS 2.0. The description is used + // as a + // title if the item has no title-tag. + if (state.getCurrentItem().getTitle() == null) { + state.getCurrentItem().setTitle( + state.getCurrentItem().getDescription()); + } + } + state.setCurrentItem(null); + } else if (state.getTagstack().size() >= 2 + && state.getContentBuf() != null) { + String content = state.getContentBuf().toString(); + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + String third = null; + if (state.getTagstack().size() >= 3) { + third = state.getThirdTag().getName(); + } + + if (top.equals(GUID) && second.equals(ITEM)) { + // some feed creators include an empty or non-standard guid-element in their feed, which should be ignored + if (!content.isEmpty()) { + state.getCurrentItem().setItemIdentifier(content); + } + } else if (top.equals(TITLE)) { + if (second.equals(ITEM)) { + state.getCurrentItem().setTitle(content); + } else if (second.equals(CHANNEL)) { + state.getFeed().setTitle(content); + } else if (second.equals(IMAGE) && third != null + && third.equals(CHANNEL)) { + state.getFeed().getImage().setTitle(content); + } + } else if (top.equals(LINK)) { + if (second.equals(CHANNEL)) { + state.getFeed().setLink(content); + } else if (second.equals(ITEM)) { + state.getCurrentItem().setLink(content); + } + } else if (top.equals(PUBDATE) && second.equals(ITEM)) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseRFC822Date(content)); + } else if (top.equals(URL) && second.equals(IMAGE) && third != null + && third.equals(CHANNEL)) { + state.getFeed().getImage().setDownload_url(content); + } else if (localName.equals(DESCR)) { + if (second.equals(CHANNEL)) { + state.getFeed().setDescription(content); + } else if (second.equals(ITEM)) { + state.getCurrentItem().setDescription(content); + } + + } else if (localName.equals(LANGUAGE)) { + state.getFeed().setLanguage(content.toLowerCase()); + } + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java new file mode 100644 index 000000000..2b4a2767d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.SimpleChapter; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import org.xml.sax.Attributes; + +import java.util.ArrayList; + +public class NSSimpleChapters extends Namespace { + public static final String NSTAG = "psc|sc"; + public static final String NSURI = "http://podlove.org/simple-chapters"; + + public static final String CHAPTERS = "chapters"; + public static final String CHAPTER = "chapter"; + public static final String START = "start"; + public static final String TITLE = "title"; + public static final String HREF = "href"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(CHAPTERS)) { + state.getCurrentItem().setChapters(new ArrayList()); + } else if (localName.equals(CHAPTER)) { + state.getCurrentItem() + .getChapters() + .add(new SimpleChapter(SyndDateUtils + .parseTimeString(attributes.getValue(START)), + attributes.getValue(TITLE), state.getCurrentItem(), + attributes.getValue(HREF))); + } + + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java new file mode 100644 index 000000000..cf118d202 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + + +public abstract class Namespace { + public static final String NSTAG = null; + public static final String NSURI = null; + + /** Called by a Feedhandler when in startElement and it detects a namespace element + * @return The SyndElement to push onto the stack + * */ + public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes); + + /** Called by a Feedhandler when in endElement and it detects a namespace element + * @return true if namespace handled the element, false if it ignored it + * */ + public abstract void handleElementEnd(String localName, HandlerState state); + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java new file mode 100644 index 000000000..8adcd2086 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +/** Defines a XML Element that is pushed on the tagstack */ +public class SyndElement { + protected String name; + protected Namespace namespace; + + public SyndElement(String name, Namespace namespace) { + this.name = name; + this.namespace = namespace; + } + + public Namespace getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java new file mode 100644 index 000000000..43fe0edb7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.syndication.namespace.atom; + +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; +import org.apache.commons.lang3.StringEscapeUtils; + +/** Represents Atom Element which contains text (content, title, summary). */ +public class AtomText extends SyndElement { + public static final String TYPE_TEXT = "text"; + public static final String TYPE_HTML = "html"; + public static final String TYPE_XHTML = "xhtml"; + + private String type; + private String content; + + public AtomText(String name, Namespace namespace, String type) { + super(name, namespace); + this.type = type; + } + + /** Processes the content according to the type and returns it. */ + public String getProcessedContent() { + if (type == null) { + return content; + } else if (type.equals(TYPE_HTML)) { + return StringEscapeUtils.unescapeHtml4(content); + } else if (type.equals(TYPE_XHTML)) { + return content; + } else { // Handle as text by default + return content; + } + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getType() { + return type; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java new file mode 100644 index 000000000..1547dc222 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java @@ -0,0 +1,194 @@ +package de.danoeh.antennapod.core.syndication.namespace.atom; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.namespace.NSRSS20; +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import org.xml.sax.Attributes; + +public class NSAtom extends Namespace { + private static final String TAG = "NSAtom"; + public static final String NSTAG = "atom"; + public static final String NSURI = "http://www.w3.org/2005/Atom"; + + private static final String FEED = "feed"; + private static final String ID = "id"; + private static final String TITLE = "title"; + private static final String ENTRY = "entry"; + private static final String LINK = "link"; + private static final String UPDATED = "updated"; + private static final String AUTHOR = "author"; + private static final String CONTENT = "content"; + private static final String IMAGE = "logo"; + private static final String SUBTITLE = "subtitle"; + private static final String PUBLISHED = "published"; + + private static final String TEXT_TYPE = "type"; + // Link + private static final String LINK_HREF = "href"; + private static final String LINK_REL = "rel"; + private static final String LINK_TYPE = "type"; + private static final String LINK_TITLE = "title"; + private static final String LINK_LENGTH = "length"; + // rel-values + private static final String LINK_REL_ALTERNATE = "alternate"; + private static final String LINK_REL_ENCLOSURE = "enclosure"; + private static final String LINK_REL_PAYMENT = "payment"; + private static final String LINK_REL_RELATED = "related"; + private static final String LINK_REL_SELF = "self"; + // type-values + private static final String LINK_TYPE_ATOM = "application/atom+xml"; + private static final String LINK_TYPE_HTML = "text/html"; + private static final String LINK_TYPE_XHTML = "application/xml+xhtml"; + + private static final String LINK_TYPE_RSS = "application/rss+xml"; + + /** + * Regexp to test whether an Element is a Text Element. + */ + private static final String isText = TITLE + "|" + CONTENT + "|" + "|" + + SUBTITLE; + + public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL; + public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(ENTRY)) { + state.setCurrentItem(new FeedItem()); + state.getItems().add(state.getCurrentItem()); + state.getCurrentItem().setFeed(state.getFeed()); + } else if (localName.matches(isText)) { + String type = attributes.getValue(TEXT_TYPE); + return new AtomText(localName, this, type); + } else if (localName.equals(LINK)) { + String href = attributes.getValue(LINK_HREF); + String rel = attributes.getValue(LINK_REL); + SyndElement parent = state.getTagstack().peek(); + if (parent.getName().matches(isFeedItem)) { + if (rel == null || rel.equals(LINK_REL_ALTERNATE)) { + state.getCurrentItem().setLink(href); + } else if (rel.equals(LINK_REL_ENCLOSURE)) { + String strSize = attributes.getValue(LINK_LENGTH); + long size = 0; + try { + if (strSize != null) { + size = Long.parseLong(strSize); + } + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) Log.d(TAG, "Length attribute could not be parsed."); + } + String type = attributes.getValue(LINK_TYPE); + if (SyndTypeUtils.enclosureTypeValid(type) + || (type = SyndTypeUtils + .getValidMimeTypeFromUrl(href)) != null) { + state.getCurrentItem().setMedia( + new FeedMedia(state.getCurrentItem(), href, + size, type) + ); + } + } else if (rel.equals(LINK_REL_PAYMENT)) { + state.getCurrentItem().setPaymentLink(href); + } + } else if (parent.getName().matches(isFeed)) { + if (rel == null || rel.equals(LINK_REL_ALTERNATE)) { + String type = attributes.getValue(LINK_TYPE); + /* + * Use as link if a) no type-attribute is given and + * feed-object has no link yet b) type of link is + * LINK_TYPE_HTML or LINK_TYPE_XHTML + */ + if ((type == null && state.getFeed().getLink() == null) + || (type != null && (type.equals(LINK_TYPE_HTML) || type.equals(LINK_TYPE_XHTML)))) { + state.getFeed().setLink(href); + } else if (type != null && (type.equals(LINK_TYPE_ATOM) || type.equals(LINK_TYPE_RSS))) { + // treat as podlove alternate feed + String title = attributes.getValue(LINK_TITLE); + if (title == null) { + title = href; + } + state.addAlternateFeedUrl(title, href); + } + } else if (rel.equals(LINK_REL_PAYMENT)) { + state.getFeed().setPaymentLink(href); + } + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(ENTRY)) { + state.setCurrentItem(null); + } + + if (state.getTagstack().size() >= 2) { + AtomText textElement = null; + String content; + if (state.getContentBuf() != null) { + content = state.getContentBuf().toString(); + } else { + content = ""; + } + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + + if (top.matches(isText)) { + textElement = (AtomText) topElement; + textElement.setContent(content); + } + + if (top.equals(ID)) { + if (second.equals(FEED)) { + state.getFeed().setFeedIdentifier(content); + } else if (second.equals(ENTRY)) { + state.getCurrentItem().setItemIdentifier(content); + } + } else if (top.equals(TITLE)) { + + if (second.equals(FEED)) { + state.getFeed().setTitle(textElement.getProcessedContent()); + } else if (second.equals(ENTRY)) { + state.getCurrentItem().setTitle( + textElement.getProcessedContent()); + } + } else if (top.equals(SUBTITLE)) { + if (second.equals(FEED)) { + state.getFeed().setDescription( + textElement.getProcessedContent()); + } + } else if (top.equals(CONTENT)) { + if (second.equals(ENTRY)) { + state.getCurrentItem().setDescription( + textElement.getProcessedContent()); + } + } else if (top.equals(UPDATED)) { + if (second.equals(ENTRY) + && state.getCurrentItem().getPubDate() == null) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseRFC3339Date(content)); + } + } else if (top.equals(PUBLISHED)) { + if (second.equals(ENTRY)) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseRFC3339Date(content)); + } + } else if (top.equals(IMAGE)) { + state.getFeed().setImage(new FeedImage(content, null)); + } + + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java new file mode 100644 index 000000000..977d92304 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java @@ -0,0 +1,153 @@ +package de.danoeh.antennapod.core.syndication.util; + +import android.util.Log; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** Parses several date formats. */ +public class SyndDateUtils { + private static final String TAG = "DateUtils"; + + private static final String[] RFC822DATES = { "dd MMM yy HH:mm:ss Z", }; + + /** RFC 3339 date format for UTC dates. */ + public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + /** RFC 3339 date format for localtime dates with offset. */ + public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ"; + + private static ThreadLocal RFC822Formatter = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat(RFC822DATES[0], Locale.US); + } + + }; + + private static ThreadLocal RFC3339Formatter = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat(RFC3339UTC, Locale.US); + } + + }; + + public static Date parseRFC822Date(String date) { + Date result = null; + if (date.contains("PDT")) { + date = date.replace("PDT", "PST8PDT"); + } + if (date.contains(",")) { + // Remove day of the week + date = date.substring(date.indexOf(",") + 1).trim(); + } + SimpleDateFormat format = RFC822Formatter.get(); + for (int i = 0; i < RFC822DATES.length; i++) { + try { + format.applyPattern(RFC822DATES[i]); + result = format.parse(date); + break; + } catch (ParseException e) { + e.printStackTrace(); + } + } + if (result == null) { + Log.e(TAG, "Unable to parse feed date correctly"); + } + + return result; + } + + public static Date parseRFC3339Date(String date) { + Date result = null; + SimpleDateFormat format = RFC3339Formatter.get(); + boolean isLocal = date.endsWith("Z"); + if (date.contains(".")) { + // remove secfrac + int fracIndex = date.indexOf("."); + String first = date.substring(0, fracIndex); + String second = null; + if (isLocal) { + second = date.substring(date.length() - 1); + } else { + if (date.contains("+")) { + second = date.substring(date.indexOf("+")); + } else { + second = date.substring(date.indexOf("-")); + } + } + + date = first + second; + } + if (isLocal) { + try { + result = format.parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + } else { + format.applyPattern(RFC3339LOCAL); + // remove last colon + StringBuffer buf = new StringBuffer(date.length() - 1); + int colonIdx = date.lastIndexOf(':'); + for (int x = 0; x < date.length(); x++) { + if (x != colonIdx) + buf.append(date.charAt(x)); + } + String bufStr = buf.toString(); + try { + result = format.parse(bufStr); + } catch (ParseException e) { + e.printStackTrace(); + Log.e(TAG, "Unable to parse date"); + } finally { + format.applyPattern(RFC3339UTC); + } + + } + + return result; + + } + + /** + * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to + * milliseconds. + */ + public static long parseTimeString(final String time) { + String[] parts = time.split(":"); + long result = 0; + int idx = 0; + if (parts.length == 3) { + // string has hours + result += Integer.valueOf(parts[idx]) * 3600000L; + idx++; + } + result += Integer.valueOf(parts[idx]) * 60000L; + idx++; + result += (Float.valueOf(parts[idx])) * 1000L; + return result; + } + + public static String formatRFC822Date(Date date) { + SimpleDateFormat format = RFC822Formatter.get(); + return format.format(date); + } + + public static String formatRFC3339Local(Date date) { + SimpleDateFormat format = RFC3339Formatter.get(); + format.applyPattern(RFC3339LOCAL); + String result = format.format(date); + format.applyPattern(RFC3339UTC); + return result; + } + + public static String formatRFC3339UTC(Date date) { + SimpleDateFormat format = RFC3339Formatter.get(); + format.applyPattern(RFC3339UTC); + return format.format(date); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java b/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java new file mode 100644 index 000000000..8d1d8ffde --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.core.syndication.util; + +import android.webkit.MimeTypeMap; +import org.apache.commons.io.FilenameUtils; + +/** Utility class for handling MIME-Types of enclosures */ +public class SyndTypeUtils { + + private final static String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*" + + "|" + "application/ogg"; + + private SyndTypeUtils() { + + } + + public static boolean enclosureTypeValid(String type) { + if (type == null) { + return false; + } else { + return type.matches(VALID_MIMETYPE); + } + } + + /** + * Should be used if mime-type of enclosure tag is not supported. This + * method will check if the mime-type of the file extension is supported. If + * the type is not supported, this method will return null. + */ + public static String getValidMimeTypeFromUrl(String url) { + if (url != null) { + String extension = FilenameUtils.getExtension(url); + if (extension != null) { + String type = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(extension); + if (type != null && enclosureTypeValid(type)) { + return type; + } + } + } + return null; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java new file mode 100644 index 000000000..d6046026f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java @@ -0,0 +1,261 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator; +import de.danoeh.antennapod.core.util.id3reader.ChapterReader; +import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentChapterReader; +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; +import org.apache.commons.io.IOUtils; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +/** Utility class for getting chapter data from media files. */ +public class ChapterUtils { + private static final String TAG = "ChapterUtils"; + + private ChapterUtils() { + } + + /** + * Uses the download URL of a media object of a feeditem to read its ID3 + * chapters. + */ + public static void readID3ChaptersFromPlayableStreamUrl(Playable p) { + if (p != null && p.getStreamUrl() != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); + InputStream in = null; + try { + URL url = new URL(p.getStreamUrl()); + ChapterReader reader = new ChapterReader(); + + in = url.openStream(); + reader.readInputStream(in); + List chapters = reader.getChapters(); + + if (chapters != null) { + Collections + .sort(chapters, new ChapterStartTimeComparator()); + processChapters(chapters, p); + if (chaptersValid(chapters)) { + p.setChapters(chapters); + Log.i(TAG, "Chapters loaded"); + } else { + Log.e(TAG, "Chapter data was invalid"); + } + } else { + Log.i(TAG, "ChapterReader could not find any ID3 chapters"); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (ID3ReaderException e) { + e.printStackTrace(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } else { + Log.e(TAG, + "Unable to read ID3 chapters: media or download URL was null"); + } + } + + /** + * Uses the file URL of a media object of a feeditem to read its ID3 + * chapters. + */ + public static void readID3ChaptersFromPlayableFileUrl(Playable p) { + if (p != null && p.localFileAvailable() && p.getLocalMediaUrl() != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); + File source = new File(p.getLocalMediaUrl()); + if (source.exists()) { + ChapterReader reader = new ChapterReader(); + InputStream in = null; + + try { + in = new BufferedInputStream(new FileInputStream(source)); + reader.readInputStream(in); + List chapters = reader.getChapters(); + + if (chapters != null) { + Collections.sort(chapters, + new ChapterStartTimeComparator()); + processChapters(chapters, p); + if (chaptersValid(chapters)) { + p.setChapters(chapters); + Log.i(TAG, "Chapters loaded"); + } else { + Log.e(TAG, "Chapter data was invalid"); + } + } else { + Log.i(TAG, + "ChapterReader could not find any ID3 chapters"); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (ID3ReaderException e) { + e.printStackTrace(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } else { + Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist"); + } + } + } + + public static void readOggChaptersFromPlayableStreamUrl(Playable media) { + if (media != null && media.streamAvailable()) { + InputStream input = null; + try { + URL url = new URL(media.getStreamUrl()); + input = url.openStream(); + if (input != null) { + readOggChaptersFromInputStream(media, input); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(input); + } + } + } + + public static void readOggChaptersFromPlayableFileUrl(Playable media) { + if (media != null && media.getLocalMediaUrl() != null) { + File source = new File(media.getLocalMediaUrl()); + if (source.exists()) { + InputStream input = null; + try { + input = new BufferedInputStream(new FileInputStream(source)); + readOggChaptersFromInputStream(media, input); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(input); + } + } + } + } + + private static void readOggChaptersFromInputStream(Playable p, + InputStream input) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Trying to read chapters from item with title " + + p.getEpisodeTitle()); + try { + VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); + reader.readInputStream(input); + List chapters = reader.getChapters(); + if (chapters != null) { + Collections.sort(chapters, new ChapterStartTimeComparator()); + processChapters(chapters, p); + if (chaptersValid(chapters)) { + p.setChapters(chapters); + Log.i(TAG, "Chapters loaded"); + } else { + Log.e(TAG, "Chapter data was invalid"); + } + } else { + Log.i(TAG, + "ChapterReader could not find any Ogg vorbis chapters"); + } + } catch (VorbisCommentReaderException e) { + e.printStackTrace(); + } + } + + /** Makes sure that chapter does a title and an item attribute. */ + private static void processChapters(List chapters, Playable p) { + for (int i = 0; i < chapters.size(); i++) { + Chapter c = chapters.get(i); + if (c.getTitle() == null) { + c.setTitle(Integer.toString(i)); + } + } + } + + private static boolean chaptersValid(List chapters) { + if (chapters.isEmpty()) { + return false; + } + for (Chapter c : chapters) { + if (c.getTitle() == null) { + return false; + } + if (c.getStart() < 0) { + return false; + } + } + return true; + } + + /** Calls getCurrentChapter with current position. */ + public static Chapter getCurrentChapter(Playable media) { + if (media.getChapters() != null) { + List chapters = media.getChapters(); + Chapter current = null; + if (chapters != null) { + current = chapters.get(0); + for (Chapter sc : chapters) { + if (sc.getStart() > media.getPosition()) { + break; + } else { + current = sc; + } + } + } + return current; + } else { + return null; + } + } + + public static void loadChaptersFromStreamUrl(Playable media) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting chapterLoader thread"); + ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media); + if (media.getChapters() == null) { + ChapterUtils.readOggChaptersFromPlayableStreamUrl(media); + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "ChapterLoaderThread has finished"); + } + + public static void loadChaptersFromFileUrl(Playable media) { + if (media.localFileAvailable()) { + ChapterUtils.readID3ChaptersFromPlayableFileUrl(media); + if (media.getChapters() == null) { + ChapterUtils.readOggChaptersFromPlayableFileUrl(media); + } + } else { + Log.e(TAG, "Could not load chapters from file url: local file not available"); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/app/src/main/java/de/danoeh/antennapod/core/util/Converter.java new file mode 100644 index 000000000..a0b514bd6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/Converter.java @@ -0,0 +1,103 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +/** Provides methods for converting various units. */ +public final class Converter { + /** Class shall not be instantiated. */ + private Converter() { + } + + /** Logging tag. */ + private static final String TAG = "Converter"; + + + /** Indicates that the value is in the Byte range.*/ + private static final int B_RANGE = 0; + /** Indicates that the value is in the Kilobyte range.*/ + private static final int KB_RANGE = 1; + /** Indicates that the value is in the Megabyte range.*/ + private static final int MB_RANGE = 2; + /** Indicates that the value is in the Gigabyte range.*/ + private static final int GB_RANGE = 3; + /** Determines the length of the number for best readability.*/ + private static final int NUM_LENGTH = 1024; + + + private static final int HOURS_MIL = 3600000; + private static final int MINUTES_MIL = 60000; + private static final int SECONDS_MIL = 1000; + + /** Takes a byte-value and converts it into a more readable + * String. + * @param input The value to convert + * @return The converted String with a unit + * */ + public static String byteToString(final long input) { + int i = 0; + int result = 0; + + for (i = 0; i < GB_RANGE + 1; i++) { + result = (int) (input / Math.pow(1024, i)); + if (result < NUM_LENGTH) { + break; + } + } + + switch (i) { + case B_RANGE: + return result + " B"; + case KB_RANGE: + return result + " KB"; + case MB_RANGE: + return result + " MB"; + case GB_RANGE: + return result + " GB"; + default: + Log.e(TAG, "Error happened in byteToString"); + return "ERROR"; + } + } + + /** Converts milliseconds to a string containing hours, minutes and seconds */ + public static String getDurationStringLong(int duration) { + int h = duration / HOURS_MIL; + int rest = duration - h * HOURS_MIL; + int m = rest / MINUTES_MIL; + rest -= m * MINUTES_MIL; + int s = rest / SECONDS_MIL; + + return String.format("%02d:%02d:%02d", h, m, s); + } + + /** Converts milliseconds to a string containing hours and minutes */ + public static String getDurationStringShort(int duration) { + int h = duration / HOURS_MIL; + int rest = duration - h * HOURS_MIL; + int m = rest / MINUTES_MIL; + + return String.format("%02d:%02d", h, m); + } + + /** Converts long duration string (HH:MM:SS) to milliseconds. */ + public static int durationStringLongToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 3) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 60 * 1000 + + Integer.valueOf(parts[2]) * 1000; + } + + /** Converts short duration string (HH:MM) to milliseconds. */ + public static int durationStringShortToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 2) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 1000 * 60; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/app/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java new file mode 100644 index 000000000..447e7d256 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java @@ -0,0 +1,52 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import de.danoeh.antennapod.R; + +/** Utility class for Download Errors. */ +public enum DownloadError { + SUCCESS(0, R.string.download_successful), + ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception), + ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type), + ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error), + ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown), + ERROR_IO_ERROR(5, R.string.download_error_io_error), + ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown), + ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown), + ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found), + ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error), + ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), + ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), + ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), + ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), + ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized); + + private final int code; + private final int resId; + + private DownloadError(int code, int resId) { + this.code = code; + this.resId = resId; + } + + /** Return DownloadError from its associated code. */ + public static DownloadError fromCode(int code) { + for (DownloadError reason : values()) { + if (reason.getCode() == code) { + return reason; + } + } + throw new IllegalArgumentException("unknown code: " + code); + } + + /** Get machine-readable code. */ + public int getCode() { + return code; + } + + /** Get a human-readable string. */ + public String getErrorString(Context context) { + return context.getString(resId); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/DuckType.java b/app/src/main/java/de/danoeh/antennapod/core/util/DuckType.java new file mode 100644 index 000000000..5d2803b84 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/DuckType.java @@ -0,0 +1,117 @@ +/* Adapted from: http://thinking-in-code.blogspot.com/2008/11/duck-typing-in-java-using-dynamic.html */ + +package de.danoeh.antennapod.core.util; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import de.danoeh.antennapod.BuildConfig; + +/** + * Allows "duck typing" or dynamic invocation based on method signature rather + * than type hierarchy. In other words, rather than checking whether something + * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck. + * + * To use first use the coerce static method to indicate the object you want to + * do Duck Typing for, then specify an interface to the to method which you want + * to coerce the type to, e.g: + * + * public interface Foo { void aMethod(); } class Bar { ... public void + * aMethod() { ... } ... } Bar bar = ...; Foo foo = + * DuckType.coerce(bar).to(Foo.class); foo.aMethod(); + * + * + */ +public class DuckType { + + private final Object objectToCoerce; + + private DuckType(Object objectToCoerce) { + this.objectToCoerce = objectToCoerce; + } + + private class CoercedProxy implements InvocationHandler { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Method delegateMethod = findMethodBySignature(method); + assert delegateMethod != null; + return delegateMethod.invoke(DuckType.this.objectToCoerce, args); + } + } + + /** + * Specify the duck typed object to coerce. + * + * @param object + * the object to coerce + * @return + */ + public static DuckType coerce(Object object) { + return new DuckType(object); + } + + /** + * Coerce the Duck Typed object to the given interface providing it + * implements all the necessary methods. + * + * @param + * @param iface + * @return an instance of the given interface that wraps the duck typed + * class + * @throws ClassCastException + * if the object being coerced does not implement all the + * methods in the given interface. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public T to(Class iface) { + if (BuildConfig.DEBUG && !iface.isInterface()) throw new AssertionError("cannot coerce object to a class, must be an interface"); + if (isA(iface)) { + return (T) iface.cast(objectToCoerce); + } + if (quacksLikeA(iface)) { + return generateProxy(iface); + } + throw new ClassCastException("Could not coerce object of type " + objectToCoerce.getClass() + " to " + iface); + } + + @SuppressWarnings("rawtypes") + private boolean isA(Class iface) { + return objectToCoerce.getClass().isInstance(iface); + } + + /** + * Determine whether the duck typed object can be used with the given + * interface. + * + * @param Type + * of the interface to check. + * @param iface + * Interface class to check + * @return true if the object will support all the methods in the interface, + * false otherwise. + */ + @SuppressWarnings("rawtypes") + public boolean quacksLikeA(Class iface) { + for (Method method : iface.getMethods()) { + if (findMethodBySignature(method) == null) { + return false; + } + } + return true; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private T generateProxy(Class iface) { + return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new CoercedProxy()); + } + + private Method findMethodBySignature(Method method) { + try { + return objectToCoerce.getClass().getMethod(method.getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + return null; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java b/app/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java new file mode 100644 index 000000000..4c23b161b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.ArrayList; +import java.util.List; + +public class EpisodeFilter { + private EpisodeFilter() { + + } + + /** Return a copy of the itemlist without items which have no media. */ + public static ArrayList getEpisodeList(List items) { + ArrayList episodes = new ArrayList(items); + for (FeedItem item : items) { + if (item.getMedia() == null) { + episodes.remove(item); + } + } + return episodes; + } + + public static int countItemsWithEpisodes(List items) { + int count = 0; + for (FeedItem item : items) { + if (item.getMedia() != null) { + count++; + } + } + return count; + } + + public static FeedItem accessEpisodeByIndex(List items, + int position) { + int count = 0; + for (FeedItem item : items) { + + if (item.getMedia() != null) { + if (count == position) { + return item; + } else { + count++; + } + } + } + return null; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java b/app/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java new file mode 100644 index 000000000..bf14cd23e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.Feed; + +import java.util.Comparator; + +/** Compares the title of two feeds for sorting. */ +public class FeedtitleComparator implements Comparator { + + @Override + public int compare(Feed lhs, Feed rhs) { + return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/app/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java new file mode 100644 index 000000000..00c023b64 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.util; + +import java.util.Arrays; + +/** Generates valid filenames for a given string. */ +public class FileNameGenerator { + + private static final char[] ILLEGAL_CHARACTERS = { '/', '\\', '?', '%', + '*', ':', '|', '"', '<', '>' }; + static { + Arrays.sort(ILLEGAL_CHARACTERS); + } + + private FileNameGenerator() { + + } + + /** + * This method will return a new string that doesn't contain any illegal + * characters of the given string. + */ + public static String generateFileName(String string) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + if (Arrays.binarySearch(ILLEGAL_CHARACTERS, c) < 0) { + builder.append(c); + } + } + return builder.toString().replaceFirst(" *$",""); + } + + public static long generateLong(final String str) { + return str.hashCode(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java b/app/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java new file mode 100644 index 000000000..c98c2d82a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.util; + +/** Thrown if a feed has invalid attribute values. */ +public class InvalidFeedException extends Exception { + + public InvalidFeedException() { + } + + public InvalidFeedException(String detailMessage) { + super(detailMessage); + } + + public InvalidFeedException(Throwable throwable) { + super(throwable); + } + + public InvalidFeedException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java new file mode 100644 index 000000000..07432d28a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.core.util; + +import java.nio.charset.Charset; +import java.util.HashMap; + +public class LangUtils { + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static HashMap languages; + static { + languages = new HashMap(); + languages.put("af", "Afrikaans"); + languages.put("sq", "Albanian"); + languages.put("sq", "Albanian"); + languages.put("eu", "Basque"); + languages.put("be", "Belarusian"); + languages.put("bg", "Bulgarian"); + languages.put("ca", "Catalan"); + languages.put("Chinese (Simplified)", "zh-cn"); + languages.put("Chinese (Traditional)", "zh-tw"); + languages.put("hr", "Croatian"); + languages.put("cs", "Czech"); + languages.put("da", "Danish"); + languages.put("nl", "Dutch"); + languages.put("nl-be", "Dutch (Belgium)"); + languages.put("nl-nl", "Dutch (Netherlands)"); + languages.put("en", "English"); + languages.put("en-au", "English (Australia)"); + languages.put("en-bz", "English (Belize)"); + languages.put("en-ca", "English (Canada)"); + languages.put("en-ie", "English (Ireland)"); + languages.put("en-jm", "English (Jamaica)"); + languages.put("en-nz", "English (New Zealand)"); + languages.put("en-ph", "English (Phillipines)"); + languages.put("en-za", "English (South Africa)"); + languages.put("en-tt", "English (Trinidad)"); + languages.put("en-gb", "English (United Kingdom)"); + languages.put("en-us", "English (United States)"); + languages.put("en-zw", "English (Zimbabwe)"); + languages.put("et", "Estonian"); + languages.put("fo", "Faeroese"); + languages.put("fi", "Finnish"); + languages.put("fr", "French"); + languages.put("fr-be", "French (Belgium)"); + languages.put("fr-ca", "French (Canada)"); + languages.put("fr-fr", "French (France)"); + languages.put("fr-lu", "French (Luxembourg)"); + languages.put("fr-mc", "French (Monaco)"); + languages.put("fr-ch", "French (Switzerland)"); + languages.put("gl", "Galician"); + languages.put("gd", "Gaelic"); + languages.put("de", "German"); + languages.put("de-at", "German (Austria)"); + languages.put("de-de", "German (Germany)"); + languages.put("de-li", "German (Liechtenstein)"); + languages.put("de-lu", "German (Luxembourg)"); + languages.put("de-ch", "German (Switzerland)"); + languages.put("el", "Greek"); + languages.put("haw", "Hawaiian"); + languages.put("hu", "Hungarian"); + languages.put("is", "Icelandic"); + languages.put("in", "Indonesian"); + languages.put("ga", "Irish"); + languages.put("it", "Italian"); + languages.put("it-it", "Italian (Italy)"); + languages.put("it-ch", "Italian (Switzerland)"); + languages.put("ja", "Japanese"); + languages.put("ko", "Korean"); + languages.put("mk", "Macedonian"); + languages.put("no", "Norwegian"); + languages.put("pl", "Polish"); + languages.put("pt", "Portugese"); + languages.put("pt-br", "Portugese (Brazil)"); + languages.put("pt-pt", "Portugese (Portugal"); + languages.put("ro", "Romanian"); + languages.put("ro-mo", "Romanian (Moldova)"); + languages.put("ro-ro", "Romanian (Romania"); + languages.put("ru", "Russian"); + languages.put("ru-mo", "Russian (Moldova)"); + languages.put("ru-ru", "Russian (Russia)"); + languages.put("sr", "Serbian"); + languages.put("sk", "Slovak"); + languages.put("sl", "Slovenian"); + languages.put("es", "Spanish"); + languages.put("es-ar", "Spanish (Argentinia)"); + languages.put("es=bo", "Spanish (Bolivia)"); + languages.put("es-cl", "Spanish (Chile)"); + languages.put("es-co", "Spanish (Colombia)"); + languages.put("es-cr", "Spanish (Costa Rica)"); + languages.put("es-do", "Spanish (Dominican Republic)"); + languages.put("es-ec", "Spanish (Ecuador)"); + languages.put("es-sv", "Spanish (El Salvador)"); + languages.put("es-gt", "Spanish (Guatemala)"); + languages.put("es-hn", "Spanish (Honduras)"); + languages.put("es-mx", "Spanish (Mexico)"); + languages.put("es-ni", "Spanish (Nicaragua)"); + languages.put("es-pa", "Spanish (Panama)"); + languages.put("es-py", "Spanish (Paraguay)"); + languages.put("es-pe", "Spanish (Peru)"); + languages.put("es-pr", "Spanish (Puerto Rico)"); + languages.put("es-es", "Spanish (Spain)"); + languages.put("es-uy", "Spanish (Uruguay)"); + languages.put("es-ve", "Spanish (Venezuela)"); + languages.put("sv", "Swedish"); + languages.put("sv-fi", "Swedish (Finland)"); + languages.put("sv-se", "Swedish (Sweden)"); + languages.put("tr", "Turkish"); + languages.put("uk", "Ukranian"); + } + + /** Finds language string for key or returns the language key if it can't be found. */ + public static String getLanguageString(String key) { + String language = languages.get(key); + if (language != null) { + return language; + } else { + return key; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java new file mode 100644 index 000000000..89bba290c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.util.Arrays; +import java.util.List; + +public class NetworkUtils { + private static final String TAG = "NetworkUtils"; + + private NetworkUtils() { + + } + + /** + * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for + * automatic downloads is disabled or the device is connected to a Wi-Fi + * network that is on the 'selected networks' list of the Wi-Fi filter for + * automatic downloads and false otherwise. + * */ + public static boolean autodownloadNetworkAvailable(Context context) { + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo != null) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Device is connected to Wi-Fi"); + if (networkInfo.isConnected()) { + if (!UserPreferences.isEnableAutodownloadWifiFilter()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Auto-dl filter is disabled"); + return true; + } else { + WifiManager wm = (WifiManager) context + .getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wm.getConnectionInfo(); + List selectedNetworks = Arrays + .asList(UserPreferences + .getAutodownloadSelectedNetworks()); + if (selectedNetworks.contains(Integer.toString(wifiInfo + .getNetworkId()))) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Current network is on the selected networks list"); + return true; + } + } + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Network for auto-dl is not available"); + return false; + } + + public static boolean networkAvailable(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java b/app/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java new file mode 100644 index 000000000..8e40ae184 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java @@ -0,0 +1,93 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Iterator; +import java.util.List; + +/** + * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that + * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List. + * QueueAccess provides an interface for accessing the queue without having to care about the type of the queue + * representation. + */ +public abstract class QueueAccess { + /** + * Returns true if the item is in the queue, false otherwise. + */ + public abstract boolean contains(long id); + + /** + * Removes the item from the queue. + * + * @return true if the queue was modified by this operation. + */ + public abstract boolean remove(long id); + + private QueueAccess() { + + } + + public static QueueAccess IDListAccess(final List ids) { + return new QueueAccess() { + @Override + public boolean contains(long id) { + return (ids != null) && ids.contains(id); + } + + @Override + public boolean remove(long id) { + return ids.remove(id); + } + + + }; + } + + public static QueueAccess ItemListAccess(final List items) { + return new QueueAccess() { + @Override + public boolean contains(long id) { + if (items == null) { + return false; + } + for (FeedItem item : items) { + if (item.getId() == id) { + return true; + } + } + return false; + } + + @Override + public boolean remove(long id) { + Iterator it = items.iterator(); + FeedItem item; + while (it.hasNext()) { + item = it.next(); + if (item.getId() == id) { + it.remove(); + return true; + } + } + return false; + } + }; + } + + public static QueueAccess NotInQueueAccess() { + return new QueueAccess() { + @Override + public boolean contains(long id) { + return false; + } + + @Override + public boolean remove(long id) { + return false; + } + }; + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java new file mode 100644 index 000000000..85f32ed50 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.content.Intent; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; + +/** Utility methods for sharing data */ +public class ShareUtils { + private static final String TAG = "ShareUtils"; + + private ShareUtils() {} + + public static void shareLink(Context context, String link) { + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("text/plain"); + i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL"); + i.putExtra(Intent.EXTRA_TEXT, link); + context.startActivity(Intent.createChooser(i, "Share URL")); + } + + public static void shareFeedItemLink(Context context, FeedItem item) { + shareLink(context, item.getLink()); + } + + public static void shareFeedDownloadLink(Context context, Feed feed) { + shareLink(context, feed.getDownload_url()); + } + + public static void shareFeedlink(Context context, Feed feed) { + shareLink(context, feed.getLink()); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java b/app/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java new file mode 100644 index 000000000..7e7c6c08b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java @@ -0,0 +1,16 @@ +package de.danoeh.antennapod.core.util; + +import java.util.concurrent.Callable; + +/** + * Created by daniel on 04.08.13. + */ +public interface ShownotesProvider { + /** + * Loads shownotes. If the shownotes have to be loaded from a file or from a + * database, it should be done in a separate thread. After the shownotes + * have been loaded, callback.onShownotesLoaded should be called. + */ + public Callable loadShownotes(); + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java new file mode 100644 index 000000000..f899c211f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java @@ -0,0 +1,66 @@ +package de.danoeh.antennapod.core.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.StatFs; +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.activity.StorageErrorActivity; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.io.File; + +/** Utility functions for handling storage errors */ +public class StorageUtils { + private static final String TAG = "StorageUtils"; + + public static boolean storageAvailable(Context context) { + File dir = UserPreferences.getDataFolder(context, null); + if (dir != null) { + return dir.exists() && dir.canRead() && dir.canWrite(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Storage not available: data folder is null"); + return false; + } + } + + /** + * Checks if external storage is available. If external storage isn't + * available, the current activity is finsished an an error activity is + * launched. + * + * @param activity + * the activity which would be finished if no storage is + * available + * @return true if external storage is available + */ + public static boolean checkStorageAvailability(Activity activity) { + boolean storageAvailable = storageAvailable(activity); + if (!storageAvailable) { + activity.finish(); + activity.startActivity(new Intent(activity, + StorageErrorActivity.class)); + } + return storageAvailable; + } + + /** Get the number of free bytes that are available on the external storage. */ + public static long getFreeSpaceAvailable() { + StatFs stat = new StatFs(UserPreferences.getDataFolder( + PodcastApp.getInstance(), null).getAbsolutePath()); + long availableBlocks; + long blockSize; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + availableBlocks = stat.getAvailableBlocksLong(); + blockSize = stat.getBlockSizeLong(); + } else { + availableBlocks = stat.getAvailableBlocks(); + blockSize = stat.getBlockSize(); + } + return availableBlocks * blockSize; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java new file mode 100644 index 000000000..72d73138d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +public class ThemeUtils { + private static final String TAG = "ThemeUtils"; + + public static int getSelectionBackgroundColor() { + switch (UserPreferences.getTheme()) { + case R.style.Theme_AntennaPod_Dark: + return R.color.selection_background_color_dark; + case R.style.Theme_AntennaPod_Light: + return R.color.selection_background_color_light; + default: + Log.e(TAG, + "getSelectionBackgroundColor could not match the current theme to any color!"); + return R.color.selection_background_color_light; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java b/app/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java new file mode 100644 index 000000000..c614abbc1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Utility methods for dealing with URL encoding. + */ +public class URIUtil { + private static final String TAG = "URIUtil"; + + private URIUtil() {} + + public static URI getURIFromRequestUrl(String source) { + // try without encoding the URI + try { + return new URI(source); + } catch (URISyntaxException e) { + if (BuildConfig.DEBUG) Log.d(TAG, "Source is not encoded, encoding now"); + } + try { + URL url = new URL(source); + return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java b/app/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java new file mode 100644 index 000000000..c707e55bc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java @@ -0,0 +1,51 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.BuildConfig; + +/** + * Provides methods for checking and editing a URL. + */ +public final class URLChecker { + + /** + * Class shall not be instantiated. + */ + private URLChecker() { + } + + /** + * Logging tag. + */ + private static final String TAG = "URLChecker"; + + /** + * Checks if URL is valid and modifies it if necessary. + * + * @param url The url which is going to be prepared + * @return The prepared url + */ + public static String prepareURL(String url) { + StringBuilder builder = new StringBuilder(); + url = StringUtils.trim(url); + if (url.startsWith("feed://")) { + if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://"); + url = url.replaceFirst("feed://", "http://"); + } else if (url.startsWith("pcast://")) { + if (BuildConfig.DEBUG) Log.d(TAG, "Replacing pcast:// with http://"); + url = url.replaceFirst("pcast://", "http://"); + } else if (url.startsWith("itpc")) { + if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://"); + url = url.replaceFirst("itpc://", "http://"); + } else if (!(url.startsWith("http://") || url.startsWith("https://"))) { + if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL"); + builder.append("http://"); + } + builder.append(url); + + return builder.toString(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java b/app/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java new file mode 100644 index 000000000..d0721ac23 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012 Roman Nurik + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.danoeh.antennapod.core.util; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; +import de.danoeh.antennapod.R; + +import static com.nineoldandroids.view.ViewPropertyAnimator.animate; + +public class UndoBarController { + private View mBarView; + private TextView mMessageView; + private ViewPropertyAnimator mBarAnimator; + private Handler mHideHandler = new Handler(); + + private UndoListener mUndoListener; + + // State objects + private Parcelable mUndoToken; + private CharSequence mUndoMessage; + + public interface UndoListener { + void onUndo(Parcelable token); + } + + public UndoBarController(View undoBarView, UndoListener undoListener) { + mBarView = undoBarView; + mBarAnimator = animate(mBarView); + mUndoListener = undoListener; + + mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message); + mBarView.findViewById(R.id.undobar_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + hideUndoBar(false); + mUndoListener.onUndo(mUndoToken); + } + }); + + hideUndoBar(true); + } + + public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) { + mUndoToken = undoToken; + mUndoMessage = message; + mMessageView.setText(mUndoMessage); + + mHideHandler.removeCallbacks(mHideRunnable); + mHideHandler.postDelayed(mHideRunnable, + mBarView.getResources().getInteger(R.integer.undobar_hide_delay)); + + mBarView.setVisibility(View.VISIBLE); + if (immediate) { + ViewHelper.setAlpha(mBarView, 1); + } else { + mBarAnimator.cancel(); + mBarAnimator + .alpha(1) + .setDuration( + mBarView.getResources() + .getInteger(android.R.integer.config_shortAnimTime)) + .setListener(null); + } + } + + public void hideUndoBar(boolean immediate) { + mHideHandler.removeCallbacks(mHideRunnable); + if (immediate) { + mBarView.setVisibility(View.GONE); + ViewHelper.setAlpha(mBarView, 0); + mUndoMessage = null; + mUndoToken = null; + + } else { + mBarAnimator.cancel(); + mBarAnimator + .alpha(0) + .setDuration(mBarView.getResources() + .getInteger(android.R.integer.config_shortAnimTime)) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBarView.setVisibility(View.GONE); + mUndoMessage = null; + mUndoToken = null; + } + }); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putCharSequence("undo_message", mUndoMessage); + outState.putParcelable("undo_token", mUndoToken); + } + + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mUndoMessage = savedInstanceState.getCharSequence("undo_message"); + mUndoToken = savedInstanceState.getParcelable("undo_token"); + + if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) { + showUndoBar(true, mUndoMessage, mUndoToken); + } + } + } + + private Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + hideUndoBar(false); + } + }; +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java new file mode 100644 index 000000000..5274ffc9e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.Chapter; + +import java.util.Comparator; + +public class ChapterStartTimeComparator implements Comparator { + + @Override + public int compare(Chapter lhs, Chapter rhs) { + if (lhs.getStart() == rhs.getStart()) { + return 0; + } else if (lhs.getStart() < rhs.getStart()) { + return -1; + } else { + return 1; + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java new file mode 100644 index 000000000..ebdbfe2a5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.service.download.DownloadStatus; + +import java.util.Comparator; + +/** Compares the completion date of two Downloadstatus objects. */ +public class DownloadStatusComparator implements Comparator { + + @Override + public int compare(DownloadStatus lhs, DownloadStatus rhs) { + return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java new file mode 100644 index 000000000..a1f3ec699 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Comparator; + +/** Compares the pubDate of two FeedItems for sorting*/ +public class FeedItemPubdateComparator implements Comparator { + + /** Returns a new instance of this comparator in reverse order. + public static FeedItemPubdateComparator newInstance() { + FeedItemPubdateComparator + }*/ + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + return rhs.getPubDate().compareTo(lhs.getPubDate()); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java new file mode 100644 index 000000000..84d244660 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Comparator; + +public class PlaybackCompletionDateComparator implements Comparator { + + public int compare(FeedItem lhs, FeedItem rhs) { + if (lhs.getMedia() != null + && lhs.getMedia().getPlaybackCompletionDate() != null + && rhs.getMedia() != null + && rhs.getMedia().getPlaybackCompletionDate() != null) { + return rhs.getMedia().getPlaybackCompletionDate() + .compareTo(lhs.getMedia().getPlaybackCompletionDate()); + } + return 0; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java new file mode 100644 index 000000000..b16e0949d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.SearchResult; + +import java.util.Comparator; + +public class SearchResultValueComparator implements Comparator { + + @Override + public int compare(SearchResult lhs, SearchResult rhs) { + return rhs.getValue() - lhs.getValue(); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java b/app/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java new file mode 100644 index 000000000..287fe1100 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util.exception; + +import de.danoeh.antennapod.core.feed.FeedMedia; + +public class MediaFileNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + private FeedMedia media; + + public MediaFileNotFoundException(String msg, FeedMedia media) { + super(msg); + this.media = media; + } + + public MediaFileNotFoundException(FeedMedia media) { + super(); + this.media = media; + } + + public FeedMedia getMedia() { + return media; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java new file mode 100644 index 000000000..5a7cfa47f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.util.flattr; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import org.shredzone.flattr4j.FlattrFactory; +import org.shredzone.flattr4j.FlattrService; +import org.shredzone.flattr4j.oauth.AccessToken; + +/** Ensures that only one instance of the FlattrService class exists at a time */ + +public class FlattrServiceCreator { + public static final String TAG = "FlattrServiceCreator"; + + private static volatile FlattrService flattrService; + + public static FlattrService getService(AccessToken token) { + return FlattrFactory.getInstance().createFlattrService(token); + } + + public static void deleteFlattrService() { + if (BuildConfig.DEBUG) Log.d(TAG, "Deleting service instance"); + flattrService = null; + } +} + diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java new file mode 100644 index 000000000..d82171d1a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.core.util.flattr; + +import java.util.Calendar; + +public class FlattrStatus { + public static final int STATUS_UNFLATTERED = 0; + public static final int STATUS_QUEUE = 1; + public static final int STATUS_FLATTRED = 2; + + private int status = STATUS_UNFLATTERED; + private Calendar lastFlattred; + + public FlattrStatus() { + status = STATUS_UNFLATTERED; + lastFlattred = Calendar.getInstance(); + } + + public FlattrStatus(long status) { + lastFlattred = Calendar.getInstance(); + fromLong(status); + } + + public void setFlattred() { + status = STATUS_FLATTRED; + lastFlattred = Calendar.getInstance(); + } + + public void setUnflattred() { + status = STATUS_UNFLATTERED; + } + + public boolean getUnflattred() { + return status == STATUS_UNFLATTERED; + } + + public void setFlattrQueue() { + if (flattrable()) + status = STATUS_QUEUE; + } + + public void fromLong(long status) { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + this.status = (int) status; + else { + this.status = STATUS_FLATTRED; + lastFlattred.setTimeInMillis(status); + } + } + + public long toLong() { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + return status; + else { + return lastFlattred.getTimeInMillis(); + } + } + + public boolean flattrable() { + Calendar firstOfMonth = Calendar.getInstance(); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) ); + } + + public boolean getFlattrQueue() { + return status == STATUS_QUEUE; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java new file mode 100644 index 000000000..515028ab6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java @@ -0,0 +1,7 @@ +package de.danoeh.antennapod.core.util.flattr; + +public interface FlattrThing { + public String getTitle(); + public String getPaymentLink(); + public FlattrStatus getFlattrStatus(); +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java new file mode 100644 index 000000000..e07ed11e9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java @@ -0,0 +1,305 @@ +package de.danoeh.antennapod.core.util.flattr; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; +import org.shredzone.flattr4j.FlattrService; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; +import org.shredzone.flattr4j.model.Thing; +import org.shredzone.flattr4j.oauth.AccessToken; +import org.shredzone.flattr4j.oauth.AndroidAuthenticator; +import org.shredzone.flattr4j.oauth.Scope; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.TimeZone; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.FlattrAuthActivity; +import de.danoeh.antennapod.core.asynctask.FlattrTokenFetcher; +import de.danoeh.antennapod.core.storage.DBWriter; + +/** + * Utility methods for doing something with flattr. + */ + +public class FlattrUtils { + private static final String TAG = "FlattrUtils"; + + private static final String HOST_NAME = "de.danoeh.antennapod"; + + private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken"; + + // Flattr URL for this app. + public static final String APP_URL = "http://antennapod.com"; + // Human-readable flattr-page. + public static final String APP_LINK = "https://flattr.com/thing/745609/"; + public static final String APP_THING_ID = "745609"; + + private static volatile AccessToken cachedToken; + + private static AndroidAuthenticator createAuthenticator() { + return new AndroidAuthenticator(HOST_NAME, BuildConfig.FLATTR_APP_KEY, + BuildConfig.FLATTR_APP_SECRET); + } + + public static void startAuthProcess(Context context) throws FlattrException { + AndroidAuthenticator auth = createAuthenticator(); + auth.setScope(EnumSet.of(Scope.FLATTR)); + Intent intent = auth.createAuthenticateIntent(); + context.startActivity(intent); + } + + private static AccessToken retrieveToken() { + if (cachedToken == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Retrieving access token"); + String token = PreferenceManager.getDefaultSharedPreferences( + PodcastApp.getInstance()) + .getString(PREF_ACCESS_TOKEN, null); + if (token != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Found access token. Caching."); + cachedToken = new AccessToken(token); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No access token found"); + return null; + } + } + return cachedToken; + + } + + /** + * Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty + */ + public static boolean hasAPICredentials() { + return StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_KEY) + && StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_SECRET); + } + + public static boolean hasToken() { + return retrieveToken() != null; + } + + public static void storeToken(AccessToken token) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Storing token"); + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(PodcastApp.getInstance()).edit(); + if (token != null) { + editor.putString(PREF_ACCESS_TOKEN, token.getToken()); + } else { + editor.putString(PREF_ACCESS_TOKEN, null); + } + editor.commit(); + cachedToken = token; + } + + public static void deleteToken() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting flattr token"); + storeToken(null); + } + + public static Thing getAppThing(Context context) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + try { + Thing thing = fs.getThing(Thing.withId(APP_THING_ID)); + return thing; + } catch (FlattrException e) { + e.printStackTrace(); + showErrorDialog(context, e.getMessage()); + return null; + } + } + + public static void clickUrl(Context context, String url) + throws FlattrException { + if (hasToken()) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + fs.click(url); + } else { + Log.e(TAG, "clickUrl was called with null access token"); + } + } + + public static List retrieveFlattredThings() + throws FlattrException { + ArrayList myFlattrs = new ArrayList(); + + if (hasToken()) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + + Calendar firstOfMonth = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + firstOfMonth.set(Calendar.MILLISECOND, 0); + firstOfMonth.set(Calendar.SECOND, 0); + firstOfMonth.set(Calendar.MINUTE, 0); + firstOfMonth.set(Calendar.HOUR_OF_DAY, 0); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + Date firstOfMonthDate = firstOfMonth.getTime(); + + // subscriptions some times get flattrd slightly before midnight - give it an hour leeway + firstOfMonthDate = new Date(firstOfMonthDate.getTime() - 60 * 60 * 1000); + + final int FLATTR_COUNT = 30; + final int FLATTR_MAXPAGE = 5; + + for (int page = 0; page < FLATTR_MAXPAGE; page++) { + for (Flattr fl : fs.getMyFlattrs(FLATTR_COUNT, page)) { + if (fl.getCreated().after(firstOfMonthDate)) + myFlattrs.add(fl); + else + break; + } + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate); + + for (Flattr fl : myFlattrs) { + Thing thing = fl.getThing(); + Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated()); + } + } + + } else { + Log.e(TAG, "retrieveFlattrdThings was called with null access token"); + } + + return myFlattrs; + } + + public static void handleCallback(Context context, Uri uri) { + AndroidAuthenticator auth = createAuthenticator(); + new FlattrTokenFetcher(context, auth, uri).executeAsync(); + } + + public static void revokeAccessToken(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Revoking access token"); + deleteToken(); + FlattrServiceCreator.deleteFlattrService(); + showRevokeDialog(context); + DBWriter.clearAllFlattrStatus(context); + } + + // ------------------------------------------------ DIALOGS + + public static void showRevokeDialog(final Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.access_revoked_title); + builder.setMessage(R.string.access_revoked_info); + builder.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.create().show(); + } + + /** + * Opens a dialog that ask the user to either connect the app with flattr or to be redirected to + * the thing's website. + * If no API credentials are available, the user will immediately be redirected to the thing's website. + * */ + public static void showNoTokenDialogOrRedirect(final Context context, final String url) { + if (hasAPICredentials()) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.no_flattr_token_title); + builder.setMessage(R.string.no_flattr_token_msg); + builder.setPositiveButton(R.string.authenticate_now_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + context.startActivity(new Intent(context, + FlattrAuthActivity.class)); + } + + } + ); + + builder.setNegativeButton(R.string.visit_website_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, + uri)); + } + + } + ); + builder.create().show(); + } else { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + } + + public static void showForbiddenDialog(final Context context, + final String url) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.action_forbidden_title); + builder.setMessage(R.string.action_forbidden_msg); + builder.setPositiveButton(R.string.authenticate_now_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + context.startActivity(new Intent(context, + FlattrAuthActivity.class)); + } + + } + ); + builder.setNegativeButton(R.string.visit_website_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, + uri)); + } + + } + ); + builder.create().show(); + } + + public static void showErrorDialog(final Context context, final String msg) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.error_label); + builder.setMessage(msg); + builder.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.create().show(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java new file mode 100644 index 000000000..2c178496e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.util.flattr; + +/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */ +public class SimpleFlattrThing implements FlattrThing { + public SimpleFlattrThing(String title, String url, FlattrStatus status) + { + this.title = title; + this.url = url; + this.status = status; + } + + public String getTitle() + { + return this.title; + } + + public String getPaymentLink() + { + return this.url; + } + + public FlattrStatus getFlattrStatus() + { + return this.status; + } + + private String title; + private String url; + private FlattrStatus status; +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java b/app/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java new file mode 100644 index 000000000..17581d3e9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.core.util.gui; + +import android.os.Parcel; +import android.os.Parcelable; +import de.danoeh.antennapod.core.feed.FeedItem; + +/** + * Used by an UndoBarController for saving a removed FeedItem + */ +public class FeedItemUndoToken implements Parcelable { + private long itemId; + private long feedId; + private int position; + + public FeedItemUndoToken(FeedItem item, int position) { + this.itemId = item.getId(); + this.feedId = item.getFeed().getId(); + this.position = position; + } + + private FeedItemUndoToken(Parcel in) { + itemId = in.readLong(); + feedId = in.readLong(); + position = in.readInt(); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public FeedItemUndoToken createFromParcel(Parcel in) { + return new FeedItemUndoToken(in); + } + + public FeedItemUndoToken[] newArray(int size) { + return new FeedItemUndoToken[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeLong(itemId); + out.writeLong(feedId); + out.writeInt(position); + } + + public long getFeedItemId() { + return itemId; + } + + public int getPosition() { + return position; + } +} + diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java new file mode 100644 index 000000000..a0bce1c79 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java @@ -0,0 +1,118 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.ID3Chapter; +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; + +public class ChapterReader extends ID3Reader { + private static final String TAG = "ID3ChapterReader"; + + private static final String FRAME_ID_CHAPTER = "CHAP"; + private static final String FRAME_ID_TITLE = "TIT2"; + private static final String FRAME_ID_LINK = "WXXX"; + + private List chapters; + private ID3Chapter currentChapter; + + @Override + public int onStartTagHeader(TagHeader header) { + chapters = new ArrayList(); + System.out.println(header.toString()); + return ID3Reader.ACTION_DONT_SKIP; + } + + @Override + public int onStartFrameHeader(FrameHeader header, InputStream input) + throws IOException, ID3ReaderException { + System.out.println(header.toString()); + if (header.getId().equals(FRAME_ID_CHAPTER)) { + if (currentChapter != null) { + if (!hasId3Chapter(currentChapter)) { + chapters.add(currentChapter); + if (BuildConfig.DEBUG) Log.d(TAG, "Found chapter: " + currentChapter); + currentChapter = null; + } + } + StringBuffer elementId = new StringBuffer(); + readISOString(elementId, input, Integer.MAX_VALUE); + char[] startTimeSource = readBytes(input, 4); + long startTime = ((int) startTimeSource[0] << 24) + | ((int) startTimeSource[1] << 16) + | ((int) startTimeSource[2] << 8) | startTimeSource[3]; + currentChapter = new ID3Chapter(elementId.toString(), startTime); + skipBytes(input, 12); + return ID3Reader.ACTION_DONT_SKIP; + } else if (header.getId().equals(FRAME_ID_TITLE)) { + if (currentChapter != null && currentChapter.getTitle() == null) { + StringBuffer title = new StringBuffer(); + readString(title, input, header.getSize()); + currentChapter + .setTitle(title.toString()); + if (BuildConfig.DEBUG) Log.d(TAG, "Found title: " + currentChapter.getTitle()); + + return ID3Reader.ACTION_DONT_SKIP; + } + } else if (header.getId().equals(FRAME_ID_LINK)) { + if (currentChapter != null) { + // skip description + int descriptionLength = readString(null, input, header.getSize()); + StringBuffer link = new StringBuffer(); + readISOString(link, input, header.getSize() - descriptionLength); + String decodedLink = URLDecoder.decode(link.toString(), "UTF-8"); + + currentChapter.setLink(decodedLink); + + if (BuildConfig.DEBUG) Log.d(TAG, "Found link: " + currentChapter.getLink()); + return ID3Reader.ACTION_DONT_SKIP; + } + } else if (header.getId().equals("APIC")) { + Log.d(TAG, header.toString()); + } + + return super.onStartFrameHeader(header, input); + } + + private boolean hasId3Chapter(ID3Chapter chapter) { + for (Chapter c : chapters) { + if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) { + return true; + } + } + return false; + } + + @Override + public void onEndTag() { + if (currentChapter != null) { + if (!hasId3Chapter(currentChapter)) { + chapters.add(currentChapter); + } + } + System.out.println("Reached end of tag"); + if (chapters != null) { + for (Chapter c : chapters) { + System.out.println(c.toString()); + } + } + } + + @Override + public void onNoTagHeaderFound() { + System.out.println("No tag header found"); + super.onNoTagHeaderFound(); + } + + public List getChapters() { + return chapters; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java new file mode 100644 index 000000000..a238c11e9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java @@ -0,0 +1,250 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * Reads the ID3 Tag of a given file. In order to use this class, you should + * create a subclass of it and overwrite the onStart* - or onEnd* - methods. + */ +public class ID3Reader { + private static final int HEADER_LENGTH = 10; + private static final int ID3_LENGTH = 3; + private static final int FRAME_ID_LENGTH = 4; + + protected static final int ACTION_SKIP = 1; + protected static final int ACTION_DONT_SKIP = 2; + + protected int readerPosition; + + private static final byte ENCODING_UTF16_WITH_BOM = 1; + private static final byte ENCODING_UTF16_WITHOUT_BOM = 2; + private static final byte ENCODING_UTF8 = 3; + + private TagHeader tagHeader; + + public ID3Reader() { + } + + public final void readInputStream(InputStream input) throws IOException, + ID3ReaderException { + int rc; + readerPosition = 0; + char[] tagHeaderSource = readBytes(input, HEADER_LENGTH); + tagHeader = createTagHeader(tagHeaderSource); + if (tagHeader == null) { + onNoTagHeaderFound(); + } else { + rc = onStartTagHeader(tagHeader); + if (rc == ACTION_SKIP) { + onEndTag(); + } else { + while (readerPosition < tagHeader.getSize()) { + FrameHeader frameHeader = createFrameHeader(readBytes( + input, HEADER_LENGTH)); + if (checkForNullString(frameHeader.getId())) { + break; + } else { + rc = onStartFrameHeader(frameHeader, input); + if (rc == ACTION_SKIP) { + + if (frameHeader.getSize() + readerPosition > tagHeader + .getSize()) { + break; + } else { + skipBytes(input, frameHeader.getSize()); + } + } + } + } + onEndTag(); + } + } + } + + /** Returns true if string only contains null-bytes. */ + private boolean checkForNullString(String s) { + if (!s.isEmpty()) { + int i = 0; + if (s.charAt(i) == 0) { + for (i = 1; i < s.length(); i++) { + if (s.charAt(i) != 0) { + return false; + } + } + return true; + } + return false; + } else { + return true; + } + + } + + /** + * Read a certain number of bytes from the given input stream. This method + * changes the readerPosition-attribute. + */ + protected char[] readBytes(InputStream input, int number) + throws IOException, ID3ReaderException { + char[] header = new char[number]; + for (int i = 0; i < number; i++) { + int b = input.read(); + readerPosition++; + if (b != -1) { + header[i] = (char) b; + } else { + throw new ID3ReaderException("Unexpected end of stream"); + } + } + return header; + } + + /** + * Skip a certain number of bytes on the given input stream. This method + * changes the readerPosition-attribute. + */ + protected void skipBytes(InputStream input, int number) throws IOException { + if (number <= 0) { + number = 1; + } + IOUtils.skipFully(input, number); + + readerPosition += number; + } + + private TagHeader createTagHeader(char[] source) throws ID3ReaderException { + boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44) + && (source[2] == 0x33); + if (source.length != HEADER_LENGTH) { + throw new ID3ReaderException("Length of header must be " + + HEADER_LENGTH); + } + if (hasTag) { + String id = new String(source, 0, ID3_LENGTH); + char version = (char) ((source[3] << 8) | source[4]); + byte flags = (byte) source[5]; + int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8) + | source[9]; + size = unsynchsafe(size); + return new TagHeader(id, size, version, flags); + } else { + return null; + } + } + + private FrameHeader createFrameHeader(char[] source) + throws ID3ReaderException { + if (source.length != HEADER_LENGTH) { + throw new ID3ReaderException("Length of header must be " + + HEADER_LENGTH); + } + String id = new String(source, 0, FRAME_ID_LENGTH); + + int size = (((int) source[4]) << 24) | (((int) source[5]) << 16) + | (((int) source[6]) << 8) | source[7]; + if (tagHeader != null && tagHeader.getVersion() >= 0x0400) { + size = unsynchsafe(size); + } + char flags = (char) ((source[8] << 8) | source[9]); + return new FrameHeader(id, size, flags); + } + + private int unsynchsafe(int in) { + int out = 0; + int mask = 0x7F000000; + + while (mask != 0) { + out >>= 1; + out |= in & mask; + mask >>= 8; + } + + return out; + } + + protected int readString(StringBuffer buffer, InputStream input, int max) throws IOException, + ID3ReaderException { + if (max > 0) { + char[] encoding = readBytes(input, 1); + max--; + + if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) { + return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account + } else if (encoding[0] == ENCODING_UTF8) { + return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account + } else { + return readISOString(buffer, input, max) + 1; // take encoding byte into account + } + } else { + if (buffer != null) { + buffer.append(""); + } + return 0; + } + } + + protected int readISOString(StringBuffer buffer, InputStream input, int max) + throws IOException, ID3ReaderException { + + int bytesRead = 0; + char c; + while (++bytesRead <= max && (c = (char) input.read()) > 0) { + if (buffer != null) { + buffer.append(c); + } + } + return bytesRead; + } + + private int readUnicodeString(StringBuffer strBuffer, InputStream input, int max, Charset charset) + throws IOException, ID3ReaderException { + byte[] buffer = new byte[max]; + int c, cZero = -1; + int i = 0; + for (; i < max; i++) { + c = input.read(); + if (c == -1) { + break; + } else if (c == 0) { + if (cZero == 0) { + // termination character found + break; + } else { + cZero = 0; + } + } else { + buffer[i] = (byte) c; + cZero = -1; + } + } + if (strBuffer != null) { + strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString()); + } + return i; + } + + public int onStartTagHeader(TagHeader header) { + return ACTION_SKIP; + } + + public int onStartFrameHeader(FrameHeader header, InputStream input) + throws IOException, ID3ReaderException { + return ACTION_SKIP; + } + + public void onEndTag() { + + } + + public void onNoTagHeaderFound() { + + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java new file mode 100644 index 000000000..0c746d7e5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.util.id3reader; + +public class ID3ReaderException extends Exception { + + public ID3ReaderException() { + } + + public ID3ReaderException(String arg0) { + super(arg0); + } + + public ID3ReaderException(Throwable arg0) { + super(arg0); + } + + public ID3ReaderException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java new file mode 100644 index 000000000..89eab1398 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java @@ -0,0 +1,17 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public class FrameHeader extends Header { + + protected char flags; + + public FrameHeader(String id, int size, char flags) { + super(id, size); + this.flags = flags; + } + + @Override + public String toString() { + return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size)); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java new file mode 100644 index 000000000..346e2893f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java @@ -0,0 +1,29 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public abstract class Header { + + protected String id; + protected int size; + + public Header(String id, int size) { + super(); + this.id = id; + this.size = size; + } + + public String getId() { + return id; + } + + public int getSize() { + return size; + } + + @Override + public String toString() { + return "Header [id=" + id + ", size=" + size + "]"; + } + + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java new file mode 100644 index 000000000..0a6b8357f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public class TagHeader extends Header { + + protected char version; + protected byte flags; + + public TagHeader(String id, int size, char version, byte flags) { + super(id, size); + this.version = version; + this.flags = flags; + } + + @Override + public String toString() { + return "TagHeader [version=" + version + ", flags=" + flags + ", id=" + + id + ", size=" + size + "]"; + } + + public char getVersion() { + return version; + } + + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedItemMenuHandler.java new file mode 100644 index 000000000..f85ac412d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedItemMenuHandler.java @@ -0,0 +1,191 @@ +package de.danoeh.antennapod.core.util.menuhandler; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.ShareUtils; + +/** + * Handles interactions with the FeedItemMenu. + */ +public class FeedItemMenuHandler { + private static final String TAG = "FeedItemMenuHandler"; + + private FeedItemMenuHandler() { + + } + + /** + * Used by the MenuHandler to access different types of menus through one + * interface + */ + public interface MenuInterface { + /** + * Implementations of this method should call findItem(id) on their + * menu-object and call setVisibility(visibility) on the returned + * MenuItem object. + */ + abstract void setItemVisibility(int id, boolean visible); + } + + /** + * This method should be called in the prepare-methods of menus. It changes + * the visibility of the menu items depending on a FeedItem's attributes. + * + * @param mi An instance of MenuInterface that the method uses to change a + * MenuItem's visibility + * @param selectedItem The FeedItem for which the menu is supposed to be prepared + * @param showExtendedMenu True if MenuItems that let the user share information about + * the FeedItem and visit its website should be set visible. This + * parameter should be set to false if the menu space is limited. + * @param queueAccess Used for testing if the queue contains the selected item + * @return Returns true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { + if (selectedItem == null) { + return false; + } + DownloadRequester requester = DownloadRequester.getInstance(); + boolean hasMedia = selectedItem.getMedia() != null; + boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); + boolean downloading = hasMedia + && requester.isDownloadingFile(selectedItem.getMedia()); + boolean notLoadedAndNotLoading = hasMedia && (!downloaded) + && (!downloading); + boolean isPlaying = hasMedia + && selectedItem.getState() == FeedItem.State.PLAYING; + + FeedItem.State state = selectedItem.getState(); + + if (!isPlaying) { + mi.setItemVisibility(R.id.skip_episode_item, false); + } + if (!downloaded || isPlaying) { + mi.setItemVisibility(R.id.play_item, false); + mi.setItemVisibility(R.id.remove_item, false); + } + if (!notLoadedAndNotLoading) { + mi.setItemVisibility(R.id.download_item, false); + } + if (!(notLoadedAndNotLoading | downloading) | isPlaying) { + mi.setItemVisibility(R.id.stream_item, false); + } + if (!downloading) { + mi.setItemVisibility(R.id.cancel_download_item, false); + } + + boolean isInQueue = queueAccess.contains(selectedItem.getId()); + if (!isInQueue || isPlaying) { + mi.setItemVisibility(R.id.remove_from_queue_item, false); + } + if (!(!isInQueue && selectedItem.getMedia() != null)) { + mi.setItemVisibility(R.id.add_to_queue_item, false); + } + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.share_link_item, false); + } + + if (!BuildConfig.DEBUG + || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { + mi.setItemVisibility(R.id.mark_unread_item, false); + } + if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { + mi.setItemVisibility(R.id.mark_read_item, false); + } + + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.visit_website_item, false); + } + + if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) { + mi.setItemVisibility(R.id.support_item, false); + } + return true; + } + + /** + * The same method as onPrepareMenu(MenuInterface, FeedItem, boolean, QueueAccess), but lets the + * caller also specify a list of menu items that should not be shown. + * + * @param excludeIds Menu item that should be excluded + * @return true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess, int... excludeIds) { + boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess); + if (rc && excludeIds != null) { + for (int id : excludeIds) { + mi.setItemVisibility(id, false); + } + } + + return rc; + } + + public static boolean onMenuItemClicked(Context context, int menuItemId, + FeedItem selectedItem) throws DownloadRequestException { + DownloadRequester requester = DownloadRequester.getInstance(); + switch (menuItemId) { + case R.id.skip_episode_item: + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + case R.id.download_item: + DBTasks.downloadFeedItems(context, selectedItem); + break; + case R.id.play_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + false); + break; + case R.id.remove_item: + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); + break; + case R.id.cancel_download_item: + requester.cancelDownload(context, selectedItem.getMedia()); + break; + case R.id.mark_read_item: + DBWriter.markItemRead(context, selectedItem, true, true); + break; + case R.id.mark_unread_item: + DBWriter.markItemRead(context, selectedItem, false, true); + break; + case R.id.add_to_queue_item: + DBWriter.addQueueItem(context, selectedItem.getId()); + break; + case R.id.remove_from_queue_item: + DBWriter.removeQueueItem(context, selectedItem.getId(), true); + break; + case R.id.stream_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + true); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedItem.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrItemIfLoggedIn(context, selectedItem); + break; + case R.id.share_link_item: + ShareUtils.shareFeedItemLink(context, selectedItem); + break; + default: + return false; + } + // Refresh menu state + + return true; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedMenuHandler.java new file mode 100644 index 000000000..757cc5f56 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedMenuHandler.java @@ -0,0 +1,86 @@ +package de.danoeh.antennapod.core.util.menuhandler; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.ShareUtils; + +/** Handles interactions with the FeedItemMenu. */ +public class FeedMenuHandler { + private static final String TAG = "FeedMenuHandler"; + + public static boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { + inflater.inflate(R.menu.feedlist, menu); + return true; + } + + public static boolean onPrepareOptionsMenu(Menu menu, Feed selectedFeed) { + if (selectedFeed == null) { + return true; + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Preparing options menu"); + menu.findItem(R.id.mark_all_read_item).setVisible( + selectedFeed.hasNewItems(true)); + if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) + menu.findItem(R.id.support_item).setVisible(true); + else + menu.findItem(R.id.support_item).setVisible(false); + MenuItem refresh = menu.findItem(R.id.refresh_item); + if (DownloadService.isRunning + && DownloadRequester.getInstance().isDownloadingFile( + selectedFeed)) { + refresh.setVisible(false); + } else { + refresh.setVisible(true); + } + + return true; + } + + /** + * NOTE: This method does not handle clicks on the 'remove feed' - item. + * + * @throws DownloadRequestException + */ + public static boolean onOptionsItemClicked(Context context, MenuItem item, + Feed selectedFeed) throws DownloadRequestException { + switch (item.getItemId()) { + case R.id.refresh_item: + DBTasks.refreshFeed(context, selectedFeed); + break; + case R.id.mark_all_read_item: + DBWriter.markFeedRead(context, selectedFeed.getId()); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedFeed.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrFeedIfLoggedIn(context, selectedFeed); + break; + case R.id.share_link_item: + ShareUtils.shareFeedlink(context, selectedFeed); + break; + case R.id.share_source_item: + ShareUtils.shareFeedDownloadLink(context, selectedFeed); + break; + default: + return false; + } + return true; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/MenuItemUtils.java new file mode 100644 index 000000000..4258c4d22 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/MenuItemUtils.java @@ -0,0 +1,31 @@ +package de.danoeh.antennapod.core.util.menuhandler; + +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuItem; + +import de.danoeh.antennapod.R; + +/** + * Utilities for menu items + */ +public class MenuItemUtils { + + public static MenuItem addSearchItem(Menu menu, SearchView searchView) { + MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); + MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + MenuItemCompat.setActionView(item, searchView); + return item; + } + + /** + * Checks if the navigation drawer of the DrawerActivity is opened. This can be useful for Fragments + * that hide their menu if the navigation drawer is open. + * + * @return True if the drawer is open, false otherwise (also if the parameter is null) + */ + public static boolean isActivityDrawerOpen(NavDrawerActivity activity) { + return activity != null && activity.isDrawerOpen(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/NavDrawerActivity.java b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/NavDrawerActivity.java new file mode 100644 index 000000000..61bf9960f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/NavDrawerActivity.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.core.util.menuhandler; + +/** + * Defines useful methods for activities that have a navigation drawer + */ +public interface NavDrawerActivity { + + public boolean isDrawerOpen(); +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java new file mode 100644 index 000000000..aafcea307 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.util.Log; +import android.view.SurfaceHolder; +import com.aocate.media.MediaPlayer; + +public class AudioPlayer extends MediaPlayer implements IPlayer { + private static final String TAG = "AudioPlayer"; + + public AudioPlayer(Context context) { + super(context); + } + + @Override + public void setScreenOnWhilePlaying(boolean screenOn) { + Log.e(TAG, "Setting screen on while playing not supported in Audio Player"); + throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player"); + + } + + @Override + public void setDisplay(SurfaceHolder sh) { + if (sh != null) { + Log.e(TAG, "Setting display not supported in Audio Player"); + throw new UnsupportedOperationException("Setting display not supported in Audio Player"); + } + } + + @Override + public void setVideoScalingMode(int mode) { + throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java new file mode 100644 index 000000000..49769f4f0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -0,0 +1,235 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.util.ChapterUtils; + +import java.util.List; +import java.util.concurrent.Callable; + +/** Represents a media file that is stored on the local storage device. */ +public class ExternalMedia implements Playable { + + public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2; + public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl"; + public static final String PREF_POSITION = "ExternalMedia.PrefPosition"; + public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType"; + + private String source; + + private String episodeTitle; + private String feedTitle; + private MediaType mediaType = MediaType.AUDIO; + private List chapters; + private int duration; + private int position; + + public ExternalMedia(String source, MediaType mediaType) { + super(); + this.source = source; + this.mediaType = mediaType; + } + + public ExternalMedia(String source, MediaType mediaType, int position) { + this(source, mediaType); + this.position = position; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(source); + dest.writeString(mediaType.toString()); + dest.writeInt(position); + } + + @Override + public void writeToPreferences(Editor prefEditor) { + prefEditor.putString(PREF_SOURCE_URL, source); + prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString()); + prefEditor.putInt(PREF_POSITION, position); + } + + @Override + public void loadMetadata() throws PlayableException { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + try { + mmr.setDataSource(source); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + throw new PlayableException( + "IllegalArgumentException when setting up MediaMetadataReceiver"); + } catch (RuntimeException e) { + // http://code.google.com/p/android/issues/detail?id=39770 + e.printStackTrace(); + throw new PlayableException( + "RuntimeException when setting up MediaMetadataRetriever"); + } + episodeTitle = mmr + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); + feedTitle = mmr + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); + try { + duration = Integer.parseInt(mmr + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); + } catch (NumberFormatException e) { + e.printStackTrace(); + throw new PlayableException("NumberFormatException when reading duration of media file"); + } + ChapterUtils.loadChaptersFromFileUrl(this); + } + + @Override + public void loadChapterMarks() { + + } + + @Override + public String getEpisodeTitle() { + return episodeTitle; + } + + @Override + public Callable loadShownotes() { + return new Callable() { + @Override + public String call() throws Exception { + return ""; + } + }; + } + + @Override + public List getChapters() { + return chapters; + } + + @Override + public String getWebsiteLink() { + return null; + } + + @Override + public String getPaymentLink() { + return null; + } + + @Override + public String getFeedTitle() { + return feedTitle; + } + + @Override + public Object getIdentifier() { + return source; + } + + @Override + public int getDuration() { + return duration; + } + + @Override + public int getPosition() { + return position; + } + + @Override + public MediaType getMediaType() { + return mediaType; + } + + @Override + public String getLocalMediaUrl() { + return source; + } + + @Override + public String getStreamUrl() { + return null; + } + + @Override + public boolean localFileAvailable() { + return true; + } + + @Override + public boolean streamAvailable() { + return false; + } + + @Override + public void saveCurrentPosition(SharedPreferences pref, int newPosition) { + SharedPreferences.Editor editor = pref.edit(); + editor.putInt(PREF_POSITION, newPosition); + position = newPosition; + editor.commit(); + } + + @Override + public void setPosition(int newPosition) { + position = newPosition; + } + + @Override + public void setDuration(int newDuration) { + duration = newDuration; + } + + @Override + public void onPlaybackStart() { + + } + + @Override + public void onPlaybackCompleted() { + + } + + @Override + public int getPlayableType() { + return PLAYABLE_TYPE_EXTERNAL_MEDIA; + } + + @Override + public void setChapters(List chapters) { + this.chapters = chapters; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public ExternalMedia createFromParcel(Parcel in) { + String source = in.readString(); + MediaType type = MediaType.valueOf(in.readString()); + int position = 0; + if (in.dataAvail() > 0) { + position = in.readInt(); + } + ExternalMedia extMedia = new ExternalMedia(source, type, position); + return extMedia; + } + + public ExternalMedia[] newArray(int size) { + return new ExternalMedia[size]; + } + }; + + @Override + public Uri getImageUri() { + if (localFileAvailable()) { + return new Uri.Builder().scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()).build(); + } else { + return null; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java new file mode 100644 index 000000000..147c7848d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.view.SurfaceHolder; + +import java.io.IOException; + +public interface IPlayer { + boolean canSetPitch(); + + boolean canSetSpeed(); + + float getCurrentPitchStepsAdjustment(); + + int getCurrentPosition(); + + float getCurrentSpeedMultiplier(); + + int getDuration(); + + float getMaxSpeedMultiplier(); + + float getMinSpeedMultiplier(); + + boolean isLooping(); + + boolean isPlaying(); + + void pause(); + + void prepare() throws IllegalStateException, IOException; + + void prepareAsync(); + + void release(); + + void reset(); + + void seekTo(int msec); + + void setAudioStreamType(int streamtype); + + void setScreenOnWhilePlaying(boolean screenOn); + + void setDataSource(String path) throws IllegalStateException, IOException, + IllegalArgumentException, SecurityException; + + void setDisplay(SurfaceHolder sh); + + void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); + + void setLooping(boolean looping); + + void setPitchStepsAdjustment(float pitchSteps); + + void setPlaybackPitch(float f); + + void setPlaybackSpeed(float f); + + void setVolume(float left, float right); + + void start(); + + void stop(); + + public void setVideoScalingMode(int mode); + + public void setWakeMode(Context context, int mode); +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java new file mode 100644 index 000000000..a3a907e48 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.media.MediaPlayer; +import de.danoeh.antennapod.R; + +/** Utility class for MediaPlayer errors. */ +public class MediaPlayerError { + + /** Get a human-readable string for a specific error code. */ + public static String getErrorString(Context context, int code) { + int resId; + switch(code) { + case MediaPlayer.MEDIA_ERROR_SERVER_DIED: + resId = R.string.playback_error_server_died; + break; + default: + resId = R.string.playback_error_unknown; + break; + } + return context.getString(resId); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java new file mode 100644 index 000000000..7ebd580f7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -0,0 +1,207 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Parcelable; +import android.util.Log; + +import java.util.List; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.ShownotesProvider; + +/** + * Interface for objects that can be played by the PlaybackService. + */ +public interface Playable extends Parcelable, + ShownotesProvider, PicassoImageResource { + + /** + * Save information about the playable in a preference so that it can be + * restored later via PlayableUtils.createInstanceFromPreferences. + * Implementations must NOT call commit() after they have written the values + * to the preferences file. + */ + public void writeToPreferences(SharedPreferences.Editor prefEditor); + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their metadata in this method. This method + * should execute as quickly as possible and NOT load chapter marks if no + * local file is available. + */ + public void loadMetadata() throws PlayableException; + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their chapter marks in this method if no + * local file was available when loadMetadata() was called. + */ + public void loadChapterMarks(); + + /** + * Returns the title of the episode that this playable represents + */ + public String getEpisodeTitle(); + + /** + * Returns a list of chapter marks or null if this Playable has no chapters. + */ + public List getChapters(); + + /** + * Returns a link to a website that is meant to be shown in a browser + */ + public String getWebsiteLink(); + + public String getPaymentLink(); + + /** + * Returns the title of the feed this Playable belongs to. + */ + public String getFeedTitle(); + + /** + * Returns a unique identifier, for example a file url or an ID from a + * database. + */ + public Object getIdentifier(); + + /** + * Return duration of object or 0 if duration is unknown. + */ + public int getDuration(); + + /** + * Return position of object or 0 if position is unknown. + */ + public int getPosition(); + + /** + * Returns the type of media. This method should return the correct value + * BEFORE loadMetadata() is called. + */ + public MediaType getMediaType(); + + /** + * Returns an url to a local file that can be played or null if this file + * does not exist. + */ + public String getLocalMediaUrl(); + + /** + * Returns an url to a file that can be streamed by the player or null if + * this url is not known. + */ + public String getStreamUrl(); + + /** + * Returns true if a local file that can be played is available. getFileUrl + * MUST return a non-null string if this method returns true. + */ + public boolean localFileAvailable(); + + /** + * Returns true if a streamable file is available. getStreamUrl MUST return + * a non-null string if this method returns true. + */ + public boolean streamAvailable(); + + /** + * Saves the current position of this object. Implementations can use the + * provided SharedPreference to save this information and retrieve it later + * via PlayableUtils.createInstanceFromPreferences. + */ + public void saveCurrentPosition(SharedPreferences pref, int newPosition); + + public void setPosition(int newPosition); + + public void setDuration(int newDuration); + + /** + * Is called by the PlaybackService when playback starts. + */ + public void onPlaybackStart(); + + /** + * Is called by the PlaybackService when playback is completed. + */ + public void onPlaybackCompleted(); + + /** + * Returns an integer that must be unique among all Playable classes. The + * return value is later used by PlayableUtils to determine the type of the + * Playable object that is restored. + */ + public int getPlayableType(); + + public void setChapters(List chapters); + + /** + * Provides utility methods for Playable objects. + */ + public static class PlayableUtils { + private static final String TAG = "PlayableUtils"; + + /** + * Restores a playable object from a sharedPreferences file. This method might load data from the database, + * depending on the type of playable that was restored. + * + * @param type An integer that represents the type of the Playable object + * that is restored. + * @param pref The SharedPreferences file from which the Playable object + * is restored + * @return The restored Playable object + */ + public static Playable createInstanceFromPreferences(Context context, int type, + SharedPreferences pref) { + // ADD new Playable types here: + switch (type) { + case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: + long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); + if (mediaId != -1) { + return DBReader.getFeedMedia(context, mediaId); + } + break; + case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: + String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, + null); + String mediaType = pref.getString( + ExternalMedia.PREF_MEDIA_TYPE, null); + if (source != null && mediaType != null) { + int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); + return new ExternalMedia(source, + MediaType.valueOf(mediaType), position); + } + break; + } + Log.e(TAG, "Could not restore Playable object from preferences"); + return null; + } + } + + public static class PlayableException extends Exception { + private static final long serialVersionUID = 1L; + + public PlayableException() { + super(); + } + + public PlayableException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public PlayableException(String detailMessage) { + super(detailMessage); + } + + public PlayableException(Throwable throwable) { + super(throwable); + } + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java new file mode 100644 index 000000000..35bd27057 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -0,0 +1,784 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.app.Activity; +import android.content.*; +import android.content.res.TypedArray; +import android.media.MediaPlayer; +import android.os.AsyncTask; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.Pair; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageButton; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; + +import java.util.concurrent.*; + +/** + * Communicates with the playback service. GUI classes should use this class to + * control playback instead of communicating with the PlaybackService directly. + */ +public abstract class PlaybackController { + private static final String TAG = "PlaybackController"; + + public static final int INVALID_TIME = -1; + + private final Activity activity; + + private PlaybackService playbackService; + private Playable media; + private PlayerStatus status; + + private ScheduledThreadPoolExecutor schedExecutor; + private static final int SCHED_EX_POOLSIZE = 1; + + protected MediaPositionObserver positionObserver; + protected ScheduledFuture positionObserverFuture; + + private boolean mediaInfoLoaded = false; + private boolean released = false; + + /** + * True if controller should reinit playback service if 'pause' button is + * pressed. + */ + private boolean reinitOnPause; + + public PlaybackController(Activity activity, boolean reinitOnPause) { + Validate.notNull(activity); + + this.activity = activity; + this.reinitOnPause = reinitOnPause; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }, new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, + ThreadPoolExecutor executor) { + Log.w(TAG, + "Rejected execution of runnable in schedExecutor"); + } + } + ); + } + + /** + * Creates a new connection to the playbackService. Should be called in the + * activity's onResume() method. + */ + public void init() { + activity.registerReceiver(statusUpdate, new IntentFilter( + PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); + + activity.registerReceiver(notificationReceiver, new IntentFilter( + PlaybackService.ACTION_PLAYER_NOTIFICATION)); + + activity.registerReceiver(shutdownReceiver, new IntentFilter( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + + if (!released) { + bindToService(); + } else { + throw new IllegalStateException( + "Can't call init() after release() has been called"); + } + } + + /** + * Should be called if the PlaybackController is no longer needed, for + * example in the activity's onStop() method. + */ + public void release() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Releasing PlaybackController"); + + try { + activity.unregisterReceiver(statusUpdate); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(notificationReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unbindService(mConnection); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(shutdownReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + cancelPositionObserver(); + schedExecutor.shutdownNow(); + media = null; + released = true; + + } + + /** + * Should be called in the activity's onPause() method. + */ + public void pause() { + mediaInfoLoaded = false; + } + + /** + * Tries to establish a connection to the PlaybackService. If it isn't + * running, the PlaybackService will be started with the last played media + * as the arguments of the launch intent. + */ + private void bindToService() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Trying to connect to service"); + AsyncTask intentLoader = new AsyncTask() { + @Override + protected Intent doInBackground(Void... voids) { + return getPlayLastPlayedMediaIntent(); + } + + @Override + protected void onPostExecute(Intent serviceIntent) { + boolean bound = false; + if (!PlaybackService.started) { + if (serviceIntent != null) { + if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service"); + activity.startService(serviceIntent); + bound = activity.bindService(serviceIntent, mConnection, 0); + } else { + status = PlayerStatus.STOPPED; + setupGUI(); + handleStatus(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "PlaybackService is running, trying to connect without start command."); + bound = activity.bindService(new Intent(activity, + PlaybackService.class), mConnection, 0); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Result for service binding: " + bound); + } + }; + intentLoader.execute(); + } + + /** + * Returns an intent that starts the PlaybackService and plays the last + * played media or null if no last played media could be found. + */ + private Intent getPlayLastPlayedMediaIntent() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Trying to restore last played media"); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(activity.getApplicationContext()); + long currentlyPlayingMedia = PlaybackPreferences + .getCurrentlyPlayingMedia(); + if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { + Playable media = PlayableUtils.createInstanceFromPreferences(activity, + (int) currentlyPlayingMedia, prefs); + if (media != null) { + Intent serviceIntent = new Intent(activity, + PlaybackService.class); + serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + serviceIntent.putExtra( + PlaybackService.EXTRA_START_WHEN_PREPARED, false); + serviceIntent.putExtra( + PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false); + boolean fileExists = media.localFileAvailable(); + boolean lastIsStream = PlaybackPreferences + .getCurrentEpisodeIsStream(); + if (!fileExists && !lastIsStream && media instanceof FeedMedia) { + DBTasks.notifyMissingFeedMediaFile( + activity, (FeedMedia) media); + } + serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, + lastIsStream || !fileExists); + return serviceIntent; + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "No last played media found"); + return null; + } + + public abstract void setupGUI(); + + private void setupPositionObserver() { + if ((positionObserverFuture != null && positionObserverFuture + .isCancelled()) + || (positionObserverFuture != null && positionObserverFuture + .isDone()) || positionObserverFuture == null) { + + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting up position observer"); + positionObserver = new MediaPositionObserver(); + positionObserverFuture = schedExecutor.scheduleWithFixedDelay( + positionObserver, MediaPositionObserver.WAITING_INTERVALL, + MediaPositionObserver.WAITING_INTERVALL, + TimeUnit.MILLISECONDS); + } + } + + private void cancelPositionObserver() { + if (positionObserverFuture != null) { + boolean result = positionObserverFuture.cancel(true); + if (BuildConfig.DEBUG) + Log.d(TAG, "PositionObserver cancelled. Result: " + result); + } + } + + public abstract void onPositionObserverUpdate(); + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + playbackService = ((PlaybackService.LocalBinder) service) + .getService(); + if (!released) { + queryService(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Connection to Service established"); + } else { + Log.i(TAG, "Connection to playback service has been established, but controller has already been released"); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + playbackService = null; + if (BuildConfig.DEBUG) + Log.d(TAG, "Disconnected from Service"); + + } + }; + + protected BroadcastReceiver statusUpdate = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received statusUpdate Intent."); + if (isConnectedToPlaybackService()) { + PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); + status = info.playerStatus; + media = info.playable; + handleStatus(); + } else { + Log.w(TAG, + "Couldn't receive status update: playbackService was null"); + bindToService(); + } + } + }; + + protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (isConnectedToPlaybackService()) { + int type = intent.getIntExtra( + PlaybackService.EXTRA_NOTIFICATION_TYPE, -1); + int code = intent.getIntExtra( + PlaybackService.EXTRA_NOTIFICATION_CODE, -1); + if (code != -1 && type != -1) { + switch (type) { + case PlaybackService.NOTIFICATION_TYPE_ERROR: + handleError(code); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE: + float progress = ((float) code) / 100; + onBufferUpdate(progress); + break; + case PlaybackService.NOTIFICATION_TYPE_RELOAD: + cancelPositionObserver(); + mediaInfoLoaded = false; + queryService(); + onReloadNotification(intent.getIntExtra( + PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); + break; + case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: + onSleepTimerUpdate(); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_START: + onBufferStart(); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_END: + onBufferEnd(); + break; + case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END: + onPlaybackEnd(); + break; + case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE: + onPlaybackSpeedChange(); + break; + } + + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Bad arguments. Won't handle intent"); + } + } else { + bindToService(); + } + } + + }; + + private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (isConnectedToPlaybackService()) { + if (StringUtils.equals(intent.getAction(), + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + release(); + onShutdownNotification(); + } + } + } + }; + + public abstract void onPlaybackSpeedChange(); + + public abstract void onShutdownNotification(); + + /** + * Called when the currently displayed information should be refreshed. + */ + public abstract void onReloadNotification(int code); + + public abstract void onBufferStart(); + + public abstract void onBufferEnd(); + + public abstract void onBufferUpdate(float progress); + + public abstract void onSleepTimerUpdate(); + + public abstract void handleError(int code); + + public abstract void onPlaybackEnd(); + + /** + * Is called whenever the PlaybackService changes it's status. This method + * should be used to update the GUI or start/cancel background threads. + */ + private void handleStatus() { + final int playResource; + final int pauseResource; + final CharSequence playText = activity.getString(R.string.play_label); + final CharSequence pauseText = activity.getString(R.string.pause_label); + + if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) { + TypedArray res = activity.obtainStyledAttributes(new int[]{ + R.attr.av_play, R.attr.av_pause}); + playResource = res.getResourceId(0, R.drawable.av_play); + pauseResource = res.getResourceId(1, R.drawable.av_pause); + res.recycle(); + } else { + playResource = R.drawable.ic_action_play_over_video; + pauseResource = R.drawable.ic_action_pause_over_video; + } + + switch (status) { + + case ERROR: + postStatusMsg(R.string.player_error_msg); + handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); + break; + case PAUSED: + clearStatusMsg(); + checkMediaInfoLoaded(); + cancelPositionObserver(); + updatePlayButtonAppearance(playResource, playText); + if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { + setScreenOn(false); + } + break; + case PLAYING: + clearStatusMsg(); + checkMediaInfoLoaded(); + if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { + onAwaitingVideoSurface(); + setScreenOn(true); + } + setupPositionObserver(); + updatePlayButtonAppearance(pauseResource, pauseText); + break; + case PREPARING: + postStatusMsg(R.string.player_preparing_msg); + checkMediaInfoLoaded(); + if (playbackService != null) { + if (playbackService.isStartWhenPrepared()) { + updatePlayButtonAppearance(pauseResource, pauseText); + } else { + updatePlayButtonAppearance(playResource, playText); + } + } + break; + case STOPPED: + postStatusMsg(R.string.player_stopped_msg); + break; + case PREPARED: + checkMediaInfoLoaded(); + postStatusMsg(R.string.player_ready_msg); + updatePlayButtonAppearance(playResource, playText); + break; + case SEEKING: + postStatusMsg(R.string.player_seeking_msg); + break; + case INITIALIZED: + checkMediaInfoLoaded(); + clearStatusMsg(); + updatePlayButtonAppearance(playResource, playText); + break; + } + } + + private void checkMediaInfoLoaded() { + mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo()); + } + + private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { + ImageButton butPlay = getPlayButton(); + butPlay.setImageResource(resource); + butPlay.setContentDescription(contentDescription); + } + + public abstract ImageButton getPlayButton(); + + public abstract void postStatusMsg(int msg); + + public abstract void clearStatusMsg(); + + public abstract boolean loadMediaInfo(); + + public abstract void onAwaitingVideoSurface(); + + /** + * Called when connection to playback service has been established or + * information has to be refreshed + */ + void queryService() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Querying service info"); + if (playbackService != null) { + status = playbackService.getStatus(); + media = playbackService.getPlayable(); + /* + if (media == null) { + Log.w(TAG, + "PlaybackService has no media object. Trying to restore last played media."); + Intent serviceIntent = getPlayLastPlayedMediaIntent(); + if (serviceIntent != null) { + activity.startService(serviceIntent); + } + } + */ + onServiceQueried(); + + setupGUI(); + handleStatus(); + // make sure that new media is loaded if it's available + mediaInfoLoaded = false; + + } else { + Log.e(TAG, + "queryService() was called without an existing connection to playbackservice"); + } + } + + public abstract void onServiceQueried(); + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public float onSeekBarProgressChanged(SeekBar seekBar, int progress, + boolean fromUser, TextView txtvPosition) { + if (fromUser && playbackService != null && media != null) { + float prog = progress / ((float) seekBar.getMax()); + int duration = media.getDuration(); + txtvPosition.setText(Converter + .getDurationStringLong((int) (prog * duration))); + return prog; + } + return 0; + + } + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public void onSeekBarStartTrackingTouch(SeekBar seekBar) { + // interrupt position Observer, restart later + cancelPositionObserver(); + } + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { + if (playbackService != null) { + playbackService.seekTo((int) (prog * media.getDuration())); + setupPositionObserver(); + } + } + + /** + * Should be implemented by classes that show a video. The default implementation + * does nothing + * + * @param enable True if the screen should be kept on, false otherwise + */ + protected void setScreenOn(boolean enable) { + + } + + public OnClickListener newOnPlayButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (playbackService != null) { + switch (status) { + case PLAYING: + playbackService.pause(true, reinitOnPause); + break; + case PAUSED: + case PREPARED: + playbackService.resume(); + break; + case PREPARING: + playbackService.setStartWhenPrepared(!playbackService + .isStartWhenPrepared()); + if (reinitOnPause + && playbackService.isStartWhenPrepared() == false) { + playbackService.reinit(); + } + break; + case INITIALIZED: + playbackService.setStartWhenPrepared(true); + playbackService.prepare(); + break; + } + } else { + Log.w(TAG, + "Play/Pause button was pressed, but playbackservice was null!"); + } + } + + }; + } + + public OnClickListener newOnRevButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (status == PlayerStatus.PLAYING) { + playbackService.seekDelta(-UserPreferences.getSeekDeltaMs()); + } + } + }; + } + + public OnClickListener newOnFFButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (status == PlayerStatus.PLAYING) { + playbackService.seekDelta(UserPreferences.getSeekDeltaMs()); + } + } + }; + } + + public boolean serviceAvailable() { + return playbackService != null; + } + + public int getPosition() { + if (playbackService != null) { + return playbackService.getCurrentPosition(); + } else { + return PlaybackService.INVALID_TIME; + } + } + + public int getDuration() { + if (playbackService != null) { + return playbackService.getDuration(); + } else { + return PlaybackService.INVALID_TIME; + } + } + + public Playable getMedia() { + return media; + } + + public boolean sleepTimerActive() { + return playbackService != null && playbackService.sleepTimerActive(); + } + + public boolean sleepTimerNotActive() { + return playbackService != null && !playbackService.sleepTimerActive(); + } + + public void disableSleepTimer() { + if (playbackService != null) { + playbackService.disableSleepTimer(); + } + } + + public long getSleepTimerTimeLeft() { + if (playbackService != null) { + return playbackService.getSleepTimerTimeLeft(); + } else { + return INVALID_TIME; + } + } + + public void setSleepTimer(long time) { + if (playbackService != null) { + playbackService.setSleepTimer(time); + } + } + + public void seekToChapter(Chapter chapter) { + if (playbackService != null) { + playbackService.seekToChapter(chapter); + } + } + + public void seekTo(int time) { + if (playbackService != null) { + playbackService.seekTo(time); + } + } + + public void setVideoSurface(SurfaceHolder holder) { + if (playbackService != null) { + playbackService.setVideoSurface(holder); + } + } + + public PlayerStatus getStatus() { + return status; + } + + public boolean canSetPlaybackSpeed() { + return playbackService != null && playbackService.canSetSpeed(); + } + + public void setPlaybackSpeed(float speed) { + if (playbackService != null) { + playbackService.setSpeed(speed); + } + } + + public float getCurrentPlaybackSpeedMultiplier() { + if (canSetPlaybackSpeed()) { + return playbackService.getCurrentPlaybackSpeed(); + } else { + return -1; + } + } + + public boolean isPlayingVideo() { + if (playbackService != null) { + return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; + } + return false; + } + + public Pair getVideoSize() { + if (playbackService != null) { + return playbackService.getVideoSize(); + } else { + return null; + } + } + + + /** + * Returns true if PlaybackController can communicate with the playback + * service. + */ + public boolean isConnectedToPlaybackService() { + return playbackService != null; + } + + public void notifyVideoSurfaceAbandoned() { + if (playbackService != null) { + playbackService.notifyVideoSurfaceAbandoned(); + } + } + + /** + * Move service into INITIALIZED state if it's paused to save bandwidth + */ + public void reinitServiceIfPaused() { + if (playbackService != null + && playbackService.isStreaming() + && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService + .getStatus() == PlayerStatus.PREPARING && playbackService + .isStartWhenPrepared() == false))) { + playbackService.reinit(); + } + } + + /** + * Refreshes the current position of the media file that is playing. + */ + public class MediaPositionObserver implements Runnable { + + public static final int WAITING_INTERVALL = 1000; + + @Override + public void run() { + if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) { + activity.runOnUiThread(new Runnable() { + + @Override + public void run() { + onPositionObserverUpdate(); + } + }); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java new file mode 100644 index 000000000..5177bbca3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java @@ -0,0 +1,161 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.Log; +import android.util.TypedValue; + +import org.apache.commons.lang3.Validate; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ShownotesProvider; + +/** + * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the + * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's + * shownotes. + *

+ * A timeline object needs a shownotesProvider from which the chapter information is retrieved and shownotes are generated. + */ +public class Timeline { + private static final String TAG = "Timeline"; + + private static final String WEBVIEW_STYLE = "@font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }"; + + + private ShownotesProvider shownotesProvider; + + + private final String colorString; + private final int pageMargin; + + public Timeline(Context context, ShownotesProvider shownotesProvider) { + if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null"); + this.shownotesProvider = shownotesProvider; + + TypedArray res = context + .getTheme() + .obtainStyledAttributes( + new int[]{android.R.attr.textColorPrimary}); + int colorResource = res.getColor(0, 0); + colorString = String.format("#%06X", + 0xFFFFFF & colorResource); + res.recycle(); + + pageMargin = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 8, context.getResources() + .getDisplayMetrics() + ); + } + + private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))"); + private static final String TIMECODE_LINK = "%s"; + private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b"); + + /** + * Applies an app-specific CSS stylesheet and adds timecode links (optional). + *

+ * This method does NOT change the original shownotes string of the shownotesProvider object and it should + * also not be changed by the caller. + * + * @param addTimecodes True if this method should add timecode links + * @return The processed HTML string. + */ + public String processShownotes(final boolean addTimecodes) { + final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null; + + // load shownotes + + String shownotes; + try { + shownotes = shownotesProvider.loadShownotes().call(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + if (shownotes == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string"); + return ""; + } + + Document document = Jsoup.parse(shownotes); + + // apply style + String styleStr = String.format(WEBVIEW_STYLE, colorString, "100%", pageMargin, + pageMargin, pageMargin, pageMargin); + document.head().appendElement("style").attr("type", "text/css").text(styleStr); + + // apply timecode links + if (addTimecodes) { + Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); + for (Element element : elementsWithTimeCodes) { + Matcher matcherLong = TIMECODE_REGEX.matcher(element.text()); + StringBuffer buffer = new StringBuffer(); + while (matcherLong.find()) { + String h = matcherLong.group(1); + String group = matcherLong.group(0); + int time = (h != null) ? Converter.durationStringLongToMs(group) : + Converter.durationStringShortToMs(group); + + String rep; + if (playable == null || playable.getDuration() > time) { + rep = String.format(TIMECODE_LINK, time, group); + } else { + rep = group; + } + matcherLong.appendReplacement(buffer, rep); + } + matcherLong.appendTail(buffer); + + element.html(buffer.toString()); + } + } + + Log.i(TAG, "Out: " + document.toString()); + return document.toString(); + } + + + /** + * Returns true if the given link is a timecode link. + */ + public static boolean isTimecodeLink(String link) { + return link != null && link.matches(TIMECODE_LINK_REGEX.pattern()); + } + + /** + * Returns the time in milliseconds that is attached to this link or -1 + * if the link is no valid timecode link. + */ + public static int getTimecodeLinkTime(String link) { + if (isTimecodeLink(link)) { + Matcher m = TIMECODE_LINK_REGEX.matcher(link); + + try { + if (m.find()) { + return Integer.valueOf(m.group(1)); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + return -1; + } + + + public void setShownotesProvider(ShownotesProvider shownotesProvider) { + Validate.notNull(shownotesProvider); + this.shownotesProvider = shownotesProvider; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/app/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java new file mode 100644 index 000000000..dc5270d8f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.media.MediaPlayer; +import android.util.Log; + +public class VideoPlayer extends MediaPlayer implements IPlayer { + private static final String TAG = "VideoPlayer"; + + @Override + public boolean canSetPitch() { + return false; + } + + @Override + public boolean canSetSpeed() { + return false; + } + + @Override + public float getCurrentPitchStepsAdjustment() { + return 1; + } + + @Override + public float getCurrentSpeedMultiplier() { + return 1; + } + + @Override + public float getMaxSpeedMultiplier() { + return 1; + } + + @Override + public float getMinSpeedMultiplier() { + return 1; + } + + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException { + Log.e(TAG, "Setting enable speed adjustment unsupported in video player"); + throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player"); + } + + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + Log.e(TAG, "Setting pitch steps adjustment unsupported in video player"); + throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player"); + } + + @Override + public void setPlaybackPitch(float f) { + Log.e(TAG, "Setting playback pitch unsupported in video player"); + throw new UnsupportedOperationException("Setting playback pitch unsupported in video player"); + } + + @Override + public void setPlaybackSpeed(float f) { + Log.e(TAG, "Setting playback speed unsupported in video player"); + throw new UnsupportedOperationException("Setting playback speed unsupported in video player"); + } + + @Override + public void setVideoScalingMode(int mode) { + super.setVideoScalingMode(mode); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java b/app/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java new file mode 100644 index 000000000..9588265b8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.core.util.syndication; + +import android.net.Uri; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here: + *

+ * http://www.rssboard.org/rss-autodiscovery + *

+ * http://blog.whatwg.org/feed-autodiscovery + */ +public class FeedDiscoverer { + + private static final String MIME_RSS = "application/rss+xml"; + private static final String MIME_ATOM = "application/atom+xml"; + + /** + * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. + * + * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if + * a title cannot be found). + */ + public Map findLinks(File in, String baseUrl) throws IOException { + return findLinks(Jsoup.parse(in, null), baseUrl); + } + + /** + * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. + * + * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if + * a title cannot be found). + */ + public Map findLinks(String in, String baseUrl) throws IOException { + return findLinks(Jsoup.parse(in), baseUrl); + } + + private Map findLinks(Document document, String baseUrl) { + Map res = new LinkedHashMap(); + Elements links = document.head().getElementsByTag("link"); + for (Element link : links) { + String rel = link.attr("rel"); + String href = link.attr("href"); + if (!StringUtils.isEmpty(href) && + (rel.equals("alternate") || rel.equals("feed"))) { + String type = link.attr("type"); + if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) { + String title = link.attr("title"); + String processedUrl = processURL(baseUrl, href); + if (processedUrl != null) { + res.put(processedUrl, + (StringUtils.isEmpty(title)) ? href : title); + } + } + } + } + return res; + } + + private String processURL(String baseUrl, String strUrl) { + Uri uri = Uri.parse(strUrl); + if (uri.isRelative()) { + Uri res = Uri.parse(baseUrl).buildUpon().path(strUrl).build(); + return (res != null) ? res.toString() : null; + } else { + return strUrl; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java new file mode 100644 index 000000000..4799d3881 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class OggInputStream extends InputStream { + private InputStream input; + + /** True if OggInputStream is currently inside an Ogg page. */ + private boolean isInPage; + private long bytesLeft; + + public OggInputStream(InputStream input) { + super(); + isInPage = false; + this.input = input; + } + + @Override + public int read() throws IOException { + if (!isInPage) { + readOggPage(); + } + + if (isInPage && bytesLeft > 0) { + int result = input.read(); + bytesLeft -= 1; + if (bytesLeft == 0) { + isInPage = false; + } + return result; + } + return -1; + } + + private void readOggPage() throws IOException { + // find OggS + int[] buffer = new int[4]; + int c = 0; + boolean isInOggS = false; + while ((c = input.read()) != -1) { + switch (c) { + case 'O': + isInOggS = true; + buffer[0] = c; + break; + case 'g': + if (buffer[1] != c) { + buffer[1] = c; + } else { + buffer[2] = c; + } + break; + case 'S': + buffer[3] = c; + break; + default: + if (isInOggS) { + Arrays.fill(buffer, 0); + isInOggS = false; + } + } + if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g' + && buffer[3] == 'S') { + break; + } + } + // read segments + IOUtils.skipFully(input, 22); + bytesLeft = 0; + int numSegments = input.read(); + for (int i = 0; i < numSegments; i++) { + bytesLeft += input.read(); + } + isInPage = true; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java new file mode 100644 index 000000000..a6934c60e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java @@ -0,0 +1,101 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import android.util.Log; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.VorbisCommentChapter; + +import java.util.ArrayList; +import java.util.List; + +public class VorbisCommentChapterReader extends VorbisCommentReader { + private static final String TAG = "VorbisCommentChapterReader"; + + private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*"; + private static final String CHAPTER_ATTRIBUTE_TITLE = "name"; + private static final String CHAPTER_ATTRIBUTE_LINK = "url"; + + private List chapters; + + public VorbisCommentChapterReader() { + } + + @Override + public void onVorbisCommentFound() { + System.out.println("Vorbis comment found"); + } + + @Override + public void onVorbisCommentHeaderFound(VorbisCommentHeader header) { + chapters = new ArrayList(); + System.out.println(header.toString()); + } + + @Override + public boolean onContentVectorKey(String content) { + return content.matches(CHAPTER_KEY); + } + + @Override + public void onContentVectorValue(String key, String value) + throws VorbisCommentReaderException { + if (BuildConfig.DEBUG) + Log.d(TAG, "Key: " + key + ", value: " + value); + String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key); + int id = VorbisCommentChapter.getIDFromKey(key); + Chapter chapter = getChapterById(id); + if (attribute == null) { + if (getChapterById(id) == null) { + // new chapter + long start = VorbisCommentChapter.getStartTimeFromValue(value); + chapter = new VorbisCommentChapter(id); + chapter.setStart(start); + chapters.add(chapter); + } else { + throw new VorbisCommentReaderException( + "Found chapter with duplicate ID (" + key + ", " + + value + ")"); + } + } else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) { + if (chapter != null) { + chapter.setTitle(value); + } + } else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) { + if (chapter != null) { + chapter.setLink(value); + } + } + } + + @Override + public void onNoVorbisCommentFound() { + System.out.println("No vorbis comment found"); + } + + @Override + public void onEndOfComment() { + System.out.println("End of comment"); + for (Chapter c : chapters) { + System.out.println(c.toString()); + } + } + + @Override + public void onError(VorbisCommentReaderException exception) { + exception.printStackTrace(); + } + + private Chapter getChapterById(long id) { + for (Chapter c : chapters) { + if (((VorbisCommentChapter) c).getVorbisCommentId() == id) { + return c; + } + } + return null; + } + + public List getChapters() { + return chapters; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java new file mode 100644 index 000000000..5f9dd0faf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; +public class VorbisCommentHeader { + private String vendorString; + private long userCommentLength; + + public VorbisCommentHeader(String vendorString, long userCommentLength) { + super(); + this.vendorString = vendorString; + this.userCommentLength = userCommentLength; + } + + @Override + public String toString() { + return "VorbisCommentHeader [vendorString=" + vendorString + + ", userCommentLength=" + userCommentLength + "]"; + } + + public String getVendorString() { + return vendorString; + } + + public long getUserCommentLength() { + return userCommentLength; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java new file mode 100644 index 000000000..9639b9c42 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java @@ -0,0 +1,194 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import org.apache.commons.io.EndianUtils; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; + + +public abstract class VorbisCommentReader { + /** Length of first page in an ogg file in bytes. */ + private static final int FIRST_PAGE_LENGTH = 58; + private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024; + private static final int PACKET_TYPE_IDENTIFICATION = 1; + private static final int PACKET_TYPE_COMMENT = 3; + + /** Called when Reader finds identification header. */ + public abstract void onVorbisCommentFound(); + + public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header); + + /** + * Is called every time the Reader finds a content vector. The handler + * should return true if it wants to handle the content vector. + */ + public abstract boolean onContentVectorKey(String content); + + /** + * Is called if onContentVectorKey returned true for the key. + * + * @throws VorbisCommentReaderException + */ + public abstract void onContentVectorValue(String key, String value) + throws VorbisCommentReaderException; + + public abstract void onNoVorbisCommentFound(); + + public abstract void onEndOfComment(); + + public abstract void onError(VorbisCommentReaderException exception); + + public void readInputStream(InputStream input) + throws VorbisCommentReaderException { + try { + // look for identification header + if (findIdentificationHeader(input)) { + + onVorbisCommentFound(); + input = new OggInputStream(input); + if (findCommentHeader(input)) { + VorbisCommentHeader commentHeader = readCommentHeader(input); + if (commentHeader != null) { + onVorbisCommentHeaderFound(commentHeader); + for (int i = 0; i < commentHeader + .getUserCommentLength(); i++) { + try { + long vectorLength = EndianUtils + .readSwappedUnsignedInteger(input); + String key = readContentVectorKey(input, + vectorLength).toLowerCase(); + boolean readValue = onContentVectorKey(key); + if (readValue) { + String value = readUTF8String( + input, + (int) (vectorLength - key.length() - 1)); + onContentVectorValue(key, value); + } else { + IOUtils.skipFully(input, + vectorLength - key.length() - 1); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + onEndOfComment(); + } + + } else { + onError(new VorbisCommentReaderException( + "No comment header found")); + } + } else { + onNoVorbisCommentFound(); + } + } catch (IOException e) { + onError(new VorbisCommentReaderException(e)); + } + } + + private String readUTF8String(InputStream input, long length) + throws IOException { + byte[] buffer = new byte[(int) length]; + + IOUtils.readFully(input, buffer); + Charset charset = Charset.forName("UTF-8"); + return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString(); + } + + /** + * Looks for an identification header in the first page of the file. If an + * identification header is found, it will be skipped completely and the + * method will return true, otherwise false. + * + * @throws IOException + */ + private boolean findIdentificationHeader(InputStream input) + throws IOException { + byte[] buffer = new byte[FIRST_PAGE_LENGTH]; + IOUtils.readFully(input, buffer); + int i; + for (i = 6; i < buffer.length; i++) { + if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o' + && buffer[i - 3] == 'r' && buffer[i - 2] == 'b' + && buffer[i - 1] == 'i' && buffer[i] == 's' + && buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) { + return true; + } + } + return false; + } + + private boolean findCommentHeader(InputStream input) throws IOException { + char[] buffer = new char["vorbis".length() + 1]; + for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) { + char c = (char) input.read(); + int dest = -1; + switch (c) { + case PACKET_TYPE_COMMENT: + dest = 0; + break; + case 'v': + dest = 1; + break; + case 'o': + dest = 2; + break; + case 'r': + dest = 3; + break; + case 'b': + dest = 4; + break; + case 'i': + dest = 5; + break; + case 's': + dest = 6; + break; + } + if (dest >= 0) { + buffer[dest] = c; + if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r' + && buffer[4] == 'b' && buffer[5] == 'i' + && buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) { + return true; + } + } else { + Arrays.fill(buffer, (char) 0); + } + } + return false; + } + + private VorbisCommentHeader readCommentHeader(InputStream input) + throws IOException, VorbisCommentReaderException { + try { + long vendorLength = EndianUtils.readSwappedUnsignedInteger(input); + String vendorName = readUTF8String(input, vendorLength); + long userCommentLength = EndianUtils + .readSwappedUnsignedInteger(input); + return new VorbisCommentHeader(vendorName, userCommentLength); + } catch (UnsupportedEncodingException e) { + throw new VorbisCommentReaderException(e); + } + } + + private String readContentVectorKey(InputStream input, long vectorLength) + throws IOException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < vectorLength; i++) { + char c = (char) input.read(); + if (c == '=') { + return buffer.toString(); + } else { + buffer.append(c); + } + } + return null; // no key found + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java new file mode 100644 index 000000000..89ab20db0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; +public class VorbisCommentReaderException extends Exception { + + public VorbisCommentReaderException() { + super(); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0, Throwable arg1) { + super(arg0, arg1); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(Throwable arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java index d1ed795dc..1585f9b86 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java @@ -13,7 +13,7 @@ import android.widget.TextView; import org.apache.commons.lang3.Validate; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; /** * Creates a new AlertDialog that displays preferences for auto-flattring to the user. diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ConfirmationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ConfirmationDialog.java deleted file mode 100644 index df71fff77..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ConfirmationDialog.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; - -/** - * Creates an AlertDialog which asks the user to confirm something. Other - * classes can handle events like confirmation or cancellation. - */ -public abstract class ConfirmationDialog { - private static final String TAG = "ConfirmationDialog"; - - Context context; - int titleId; - int messageId; - - public ConfirmationDialog(Context context, int titleId, int messageId) { - this.context = context; - this.titleId = titleId; - this.messageId = messageId; - } - - public void onCancelButtonPressed(DialogInterface dialog) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Dialog was cancelled"); - dialog.dismiss(); - } - - public abstract void onConfirmButtonPressed(DialogInterface dialog); - - public final AlertDialog createNewDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(titleId); - builder.setMessage(messageId); - builder.setPositiveButton(R.string.confirm_label, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - onConfirmButtonPressed(dialog); - } - }); - builder.setNegativeButton(R.string.cancel_label, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - onCancelButtonPressed(dialog); - } - }); - builder.setOnCancelListener(new DialogInterface.OnCancelListener() { - - @Override - public void onCancel(DialogInterface dialog) { - onCancelButtonPressed(dialog); - } - }); - return builder.create(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java b/app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java deleted file mode 100644 index e363a6911..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/DownloadRequestErrorDialogCreator.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import de.danoeh.antennapod.R; - -/** Creates Alert Dialogs if a DownloadRequestException has happened. */ -public class DownloadRequestErrorDialogCreator { - private DownloadRequestErrorDialogCreator() { - } - - public static void newRequestErrorDialog(Context context, - String errorMessage) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setNeutralButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }) - .setTitle(R.string.download_error_request_error) - .setMessage( - context.getString(R.string.download_request_error_dialog_message_prefix) - + errorMessage); - builder.create().show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java index 7384463de..e62daa08b 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java @@ -33,16 +33,16 @@ 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; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.ShownotesProvider; +import de.danoeh.antennapod.core.util.menuhandler.FeedItemMenuHandler; /** * Shows information about a specific FeedItem and provides actions like playing, downloading, etc. diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java index a9c596d2e..16fb77f2a 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java @@ -10,8 +10,8 @@ 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; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; /** * Creates a dialog that lets the user change the hostname for the gpodder.net service. diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java deleted file mode 100644 index bbd514640..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -import android.view.View; -import android.view.Window; -import android.view.inputmethod.InputMethodManager; -import android.widget.*; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; - -import java.util.concurrent.TimeUnit; - -public abstract class TimeDialog extends Dialog { - private static final String TAG = "TimeDialog"; - - private static final int DEFAULT_SPINNER_POSITION = 1; - - private Context context; - - private EditText etxtTime; - private Spinner spTimeUnit; - private Button butConfirm; - private Button butCancel; - - private TimeUnit[] units = {TimeUnit.SECONDS, TimeUnit.MINUTES, - TimeUnit.HOURS}; - - public TimeDialog(Context context, int titleTextId, int leftButtonTextId) { - super(context); - this.context = context; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); - String[] spinnerContent = new String[]{context.getString(R.string.time_unit_seconds), - context.getString(R.string.time_unit_minutes), - context.getString(R.string.time_unit_hours)}; - - setContentView(R.layout.time_dialog); - etxtTime = (EditText) findViewById(R.id.etxtTime); - spTimeUnit = (Spinner) findViewById(R.id.spTimeUnit); - butConfirm = (Button) findViewById(R.id.butConfirm); - butCancel = (Button) findViewById(R.id.butCancel); - - butConfirm.setText(R.string.set_sleeptimer_label); - butCancel.setText(R.string.cancel_label); - setTitle(R.string.set_sleeptimer_label); - ArrayAdapter 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 index b009e76a7..8eba51540 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java @@ -7,7 +7,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import java.util.Arrays; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/feed/Chapter.java b/app/src/main/java/de/danoeh/antennapod/feed/Chapter.java deleted file mode 100644 index d6151ee9f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/Chapter.java +++ /dev/null @@ -1,55 +0,0 @@ -package de.danoeh.antennapod.feed; - -public abstract class Chapter extends FeedComponent { - - /** Defines starting point in milliseconds. */ - protected long start; - protected String title; - protected String link; - - public Chapter() { - } - - public Chapter(long start) { - super(); - this.start = start; - } - - public Chapter(long start, String title, FeedItem item, String link) { - super(); - this.start = start; - this.title = title; - this.link = link; - } - - public abstract int getChapterType(); - - public long getStart() { - return start; - } - - public String getTitle() { - return title; - } - - public String getLink() { - return link; - } - - public void setStart(long start) { - this.start = start; - } - - public void setTitle(String title) { - this.title = title; - } - - public void setLink(String link) { - this.link = link; - } - - @Override - public String getHumanReadableIdentifier() { - return title; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java b/app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java deleted file mode 100644 index 5fb72048e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/EventDistributor.java +++ /dev/null @@ -1,140 +0,0 @@ -package de.danoeh.antennapod.feed; - -import android.os.Handler; -import android.util.Log; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.BuildConfig; - -import java.util.AbstractQueue; -import java.util.Observable; -import java.util.Observer; -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * Notifies its observers about changes in the feed database. Observers can - * register by retrieving an instance of this class and registering an - * EventListener. When new events arrive, the EventDistributor will process the - * event queue in a handler that runs on the main thread. The observers will only - * be notified once if the event queue contains multiple elements. - * - * Events can be sent with the send* methods. - */ -public class EventDistributor extends Observable { - private static final String TAG = "EventDistributor"; - - public static final int FEED_LIST_UPDATE = 1; - public static final int UNREAD_ITEMS_UPDATE = 2; - public static final int QUEUE_UPDATE = 4; - public static final int DOWNLOADLOG_UPDATE = 8; - public static final int PLAYBACK_HISTORY_UPDATE = 16; - public static final int DOWNLOAD_QUEUED = 32; - public static final int DOWNLOAD_HANDLED = 64; - - private Handler handler; - private AbstractQueue 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 deleted file mode 100644 index b5415c69c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/Feed.java +++ /dev/null @@ -1,445 +0,0 @@ -package de.danoeh.antennapod.feed; - -import android.content.Context; -import android.net.Uri; - -import de.danoeh.antennapod.asynctask.PicassoImageResource; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.EpisodeFilter; -import de.danoeh.antennapod.util.flattr.FlattrStatus; -import de.danoeh.antennapod.util.flattr.FlattrThing; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * Data Object for a whole feed - * - * @author daniel - */ -public class Feed extends FeedFile implements FlattrThing, PicassoImageResource { - public static final int FEEDFILETYPE_FEED = 0; - public static final String TYPE_RSS2 = "rss"; - public static final String TYPE_RSS091 = "rss"; - public static final String TYPE_ATOM1 = "atom"; - - private String title; - /** - * Contains 'id'-element in Atom feed. - */ - private String feedIdentifier; - /** - * Link to the website. - */ - private String link; - private String description; - private String language; - /** - * Name of the author - */ - private String author; - private FeedImage image; - private List 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 deleted file mode 100644 index 48b243770..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/FeedComponent.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.danoeh.antennapod.feed; - -/** - * Represents every possible component of a feed - * - * @author daniel - */ -public abstract class FeedComponent { - - protected long id; - - public FeedComponent() { - super(); - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - /** - * Update this FeedComponent's attributes with the attributes from another - * FeedComponent. This method should only update attributes which where read from - * the feed. - */ - public void updateFromOther(FeedComponent other) { - } - - /** - * Compare's this FeedComponent's attribute values with another FeedComponent's - * attribute values. This method will only compare attributes which were - * read from the feed. - * - * @return true if attribute values are different, false otherwise - */ - public boolean compareWithOther(FeedComponent other) { - return false; - } - - - /** - * Should return a non-null, human-readable String so that the item can be - * identified by the user. Can be title, download-url, etc. - */ - public abstract String getHumanReadableIdentifier(); - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - FeedComponent that = (FeedComponent) o; - - if (id != that.id) return false; - - return true; - } - - @Override - public int hashCode() { - return (int) (id ^ (id >>> 32)); - } -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java deleted file mode 100644 index a05533ebc..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/FeedFile.java +++ /dev/null @@ -1,105 +0,0 @@ -package de.danoeh.antennapod.feed; - -import java.io.File; - -/** - * Represents a component of a Feed that has to be downloaded - */ -public abstract class FeedFile extends FeedComponent { - - protected String file_url; - protected String download_url; - protected boolean downloaded; - - /** - * Creates a new FeedFile object. - * - * @param file_url The location of the FeedFile. If this is null, the downloaded-attribute - * will automatically be set to false. - * @param download_url The location where the FeedFile can be downloaded. - * @param downloaded true if the FeedFile has been downloaded, false otherwise. This parameter - * will automatically be interpreted as false if the file_url is null. - */ - public FeedFile(String file_url, String download_url, boolean downloaded) { - super(); - this.file_url = file_url; - this.download_url = download_url; - this.downloaded = (file_url != null) && downloaded; - } - - public FeedFile() { - this(null, null, false); - } - - public abstract int getTypeAsInt(); - - /** - * Update this FeedFile's attributes with the attributes from another - * FeedFile. This method should only update attributes which where read from - * the feed. - */ - public void updateFromOther(FeedFile other) { - super.updateFromOther(other); - this.download_url = other.download_url; - } - - /** - * Compare's this FeedFile's attribute values with another FeedFile's - * attribute values. This method will only compare attributes which were - * read from the feed. - * - * @return true if attribute values are different, false otherwise - */ - public boolean compareWithOther(FeedFile other) { - if (super.compareWithOther(other)) { - return true; - } - if (!download_url.equals(other.download_url)) { - return true; - } - return false; - } - - /** - * Returns true if the file exists at file_url. - */ - public boolean fileExists() { - if (file_url == null) { - return false; - } else { - File f = new File(file_url); - return f.exists(); - } - } - - public String getFile_url() { - return file_url; - } - - /** - * Changes the file_url of this FeedFile. Setting this value to - * null will also set the downloaded-attribute to false. - */ - public void setFile_url(String file_url) { - this.file_url = file_url; - if (file_url == null) { - downloaded = false; - } - } - - public String getDownload_url() { - return download_url; - } - - public void setDownload_url(String download_url) { - this.download_url = download_url; - } - - public boolean isDownloaded() { - return downloaded; - } - - public void setDownloaded(boolean downloaded) { - this.downloaded = downloaded; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java deleted file mode 100644 index c588f5e71..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/FeedImage.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.danoeh.antennapod.feed; - -import android.net.Uri; - -import de.danoeh.antennapod.asynctask.PicassoImageResource; - -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; - - - -public class FeedImage extends FeedFile implements PicassoImageResource { - public static final int FEEDFILETYPE_FEEDIMAGE = 1; - - protected String title; - protected FeedComponent owner; - - public FeedImage(String download_url, String title) { - super(null, download_url, false); - this.download_url = download_url; - this.title = title; - } - - public FeedImage(long id, String title, String file_url, - String download_url, boolean downloaded) { - super(file_url, download_url, downloaded); - this.id = id; - this.title = title; - } - - @Override - public String getHumanReadableIdentifier() { - if (owner != null && owner.getHumanReadableIdentifier() != null) { - return owner.getHumanReadableIdentifier(); - } else { - return download_url; - } - } - - @Override - public int getTypeAsInt() { - return FEEDFILETYPE_FEEDIMAGE; - } - - public FeedImage() { - super(); - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public FeedComponent getOwner() { - return owner; - } - - public void setOwner(FeedComponent owner) { - this.owner = owner; - } - - @Override - public Uri getImageUri() { - if (file_url != null && downloaded) { - return Uri.fromFile(new File(file_url)); - } else { - return null; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java b/app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java deleted file mode 100644 index 78091ea33..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/FeedItem.java +++ /dev/null @@ -1,333 +0,0 @@ -package de.danoeh.antennapod.feed; - -import android.net.Uri; - -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.asynctask.PicassoImageResource; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.util.ShownotesProvider; -import de.danoeh.antennapod.util.flattr.FlattrStatus; -import de.danoeh.antennapod.util.flattr.FlattrThing; - -import java.io.InputStream; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; - -/** - * Data Object for a XML message - * - * @author daniel - */ -public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource { - - /** - * The id/guid that can be found in the rss/atom feed. Might not be set. - */ - private String itemIdentifier; - private String title; - /** - * The description of a feeditem. - */ - private String description; - /** - * The content of the content-encoded tag of a feeditem. - */ - private String contentEncoded; - - private String link; - private Date pubDate; - private FeedMedia media; - - private Feed feed; - private long feedId; - - private boolean read; - private String paymentLink; - private FlattrStatus flattrStatus; - private List 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 deleted file mode 100644 index 9298ebe8a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/FeedMedia.java +++ /dev/null @@ -1,411 +0,0 @@ -package de.danoeh.antennapod.feed; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; - -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.ChapterUtils; -import de.danoeh.antennapod.util.playback.Playable; - -public class FeedMedia extends FeedFile implements Playable { - private static final String TAG = "FeedMedia"; - - public static final int FEEDFILETYPE_FEEDMEDIA = 2; - public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; - - public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; - public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; - - private int duration; - private int position; // Current position in file - private int played_duration; // How many ms of this file have been played (for autoflattring) - private long size; // File size in Byte - private String mime_type; - private volatile FeedItem item; - private Date playbackCompletionDate; - - /* Used for loading item when restoring from parcel. */ - private long itemID; - - public FeedMedia(FeedItem i, String download_url, long size, - String mime_type) { - super(null, download_url, false); - this.item = i; - this.size = size; - this.mime_type = mime_type; - } - - public FeedMedia(long id, FeedItem item, int duration, int position, - long size, String mime_type, String file_url, String download_url, - boolean downloaded, Date playbackCompletionDate, int played_duration) { - super(file_url, download_url, downloaded); - this.id = id; - this.item = item; - this.duration = duration; - this.position = position; - this.played_duration = played_duration; - this.size = size; - this.mime_type = mime_type; - this.playbackCompletionDate = playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); - } - - public FeedMedia(long id, FeedItem item) { - super(); - this.id = id; - this.item = item; - } - - @Override - public String getHumanReadableIdentifier() { - if (item != null && item.getTitle() != null) { - return item.getTitle(); - } else { - return download_url; - } - } - - /** - * Uses mimetype to determine the type of media. - */ - public MediaType getMediaType() { - if (mime_type == null || mime_type.isEmpty()) { - return MediaType.UNKNOWN; - } else { - if (mime_type.startsWith("audio")) { - return MediaType.AUDIO; - } else if (mime_type.startsWith("video")) { - return MediaType.VIDEO; - } else if (mime_type.equals("application/ogg")) { - return MediaType.AUDIO; - } - } - return MediaType.UNKNOWN; - } - - public void updateFromOther(FeedMedia other) { - super.updateFromOther(other); - if (other.size > 0) { - size = other.size; - } - if (other.mime_type != null) { - mime_type = other.mime_type; - } - } - - public boolean compareWithOther(FeedMedia other) { - if (super.compareWithOther(other)) { - return true; - } - if (other.mime_type != null) { - if (mime_type == null || !mime_type.equals(other.mime_type)) { - return true; - } - } - if (other.size > 0 && other.size != size) { - return true; - } - return false; - } - - /** - * Reads playback preferences to determine whether this FeedMedia object is - * currently being played. - */ - public boolean isPlaying() { - return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA - && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; - } - - @Override - public int getTypeAsInt() { - return FEEDFILETYPE_FEEDMEDIA; - } - - public int getDuration() { - return duration; - } - - public void setDuration(int duration) { - this.duration = duration; - } - - public int getPlayedDuration() { - return played_duration; - } - - public void setPlayedDuration(int played_duration) { - this.played_duration = played_duration; - } - - public int getPosition() { - return position; - } - - public void setPosition(int position) { - this.position = position; - } - - public long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - - public String getMime_type() { - return mime_type; - } - - public void setMime_type(String mime_type) { - this.mime_type = mime_type; - } - - public FeedItem getItem() { - return item; - } - - /** - * Sets the item object of this FeedMedia. If the given - * FeedItem object is not null, it's 'media'-attribute value - * will also be set to this media object. - */ - public void setItem(FeedItem item) { - this.item = item; - if (item != null && item.getMedia() != this) { - item.setMedia(this); - } - } - - public Date getPlaybackCompletionDate() { - return playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); - } - - public void setPlaybackCompletionDate(Date playbackCompletionDate) { - this.playbackCompletionDate = playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); - } - - public boolean isInProgress() { - return (this.position > 0); - } - - public FeedImage getImage() { - if (item != null) { - return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage(); - } - return null; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(id); - dest.writeLong(item.getId()); - - dest.writeInt(duration); - dest.writeInt(position); - dest.writeLong(size); - dest.writeString(mime_type); - dest.writeString(file_url); - dest.writeString(download_url); - dest.writeByte((byte) ((downloaded) ? 1 : 0)); - dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); - dest.writeInt(played_duration); - } - - @Override - public void writeToPreferences(Editor prefEditor) { - prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); - prefEditor.putLong(PREF_MEDIA_ID, id); - } - - @Override - public void loadMetadata() throws PlayableException { - if (item == null && itemID != 0) { - item = DBReader.getFeedItem(PodcastApp.getInstance(), itemID); - } - } - - @Override - public void loadChapterMarks() { - if (getChapters() == null && !localFileAvailable()) { - ChapterUtils.loadChaptersFromStreamUrl(this); - if (getChapters() != null && item != null) { - DBWriter.setFeedItem(PodcastApp.getInstance(), - item); - } - } - - } - - @Override - public String getEpisodeTitle() { - if (item == null) { - return null; - } - if (getItem().getTitle() != null) { - return getItem().getTitle(); - } else { - return getItem().getIdentifyingValue(); - } - } - - @Override - public List 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 deleted file mode 100644 index 29bc5ef0c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/FeedPreferences.java +++ /dev/null @@ -1,89 +0,0 @@ -package de.danoeh.antennapod.feed; - -import android.content.Context; -import de.danoeh.antennapod.storage.DBWriter; -import org.apache.commons.lang3.StringUtils; - -/** - * Contains preferences for a single feed. - */ -public class FeedPreferences { - - private long feedID; - private boolean autoDownload; - private String username; - private String password; - - public FeedPreferences(long feedID, boolean autoDownload, String username, String password) { - this.feedID = feedID; - this.autoDownload = autoDownload; - this.username = username; - this.password = password; - } - - - /** - * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the - * comparison. - * - * @return True if the two objects are different. - */ - public boolean compareWithOther(FeedPreferences other) { - if (other == null) - return true; - if (!StringUtils.equals(username, other.username)) { - return true; - } - if (!StringUtils.equals(password, other.password)) { - return true; - } - return false; - } - - /** - * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded - * from the update. - */ - public void updateFromOther(FeedPreferences other) { - if (other == null) - return; - this.username = other.username; - this.password = other.password; - } - - public long getFeedID() { - return feedID; - } - - public void setFeedID(long feedID) { - this.feedID = feedID; - } - - public boolean getAutoDownload() { - return autoDownload; - } - - public void setAutoDownload(boolean autoDownload) { - this.autoDownload = autoDownload; - } - - public void save(Context context) { - DBWriter.setFeedPreferences(context, this); - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java b/app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java deleted file mode 100644 index 6dde7854e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/ID3Chapter.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.danoeh.antennapod.feed; - -public class ID3Chapter extends Chapter { - public static final int CHAPTERTYPE_ID3CHAPTER = 2; - - /** - * Identifies the chapter in its ID3 tag. This attribute does not have to be - * store in the DB and is only used for parsing. - */ - private String id3ID; - - public ID3Chapter(String id3ID, long start) { - super(start); - this.id3ID = id3ID; - } - - public ID3Chapter(long start, String title, FeedItem item, String link) { - super(start, title, item, link); - } - - @Override - public String toString() { - return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start=" - + start + ", url=" + link + "]"; - } - - @Override - public int getChapterType() { - return CHAPTERTYPE_ID3CHAPTER; - } - - public String getId3ID() { - return id3ID; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/MediaType.java b/app/src/main/java/de/danoeh/antennapod/feed/MediaType.java deleted file mode 100644 index 324d0a221..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/MediaType.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.danoeh.antennapod.feed; - -public enum MediaType { - AUDIO, VIDEO, UNKNOWN -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java b/app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java deleted file mode 100644 index 1cba389ec..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/SearchResult.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.danoeh.antennapod.feed; - -public class SearchResult { - private FeedComponent component; - /** Additional information (e.g. where it was found) */ - private String subtitle; - /** Higher value means more importance */ - private int value; - - public SearchResult(FeedComponent component, int value, String subtitle) { - super(); - this.component = component; - this.value = value; - this.subtitle = subtitle; - } - - public FeedComponent getComponent() { - return component; - } - - public String getSubtitle() { - return subtitle; - } - - public void setSubtitle(String subtitle) { - this.subtitle = subtitle; - } - - public int getValue() { - return value; - } - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java b/app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java deleted file mode 100644 index 3dab1b74d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/SimpleChapter.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.danoeh.antennapod.feed; - -public class SimpleChapter extends Chapter { - public static final int CHAPTERTYPE_SIMPLECHAPTER = 0; - - public SimpleChapter(long start, String title, FeedItem item, String link) { - super(start, title, item, link); - } - - @Override - public int getChapterType() { - return CHAPTERTYPE_SIMPLECHAPTER; - } - - public void updateFromOther(SimpleChapter other) { - super.updateFromOther(other); - start = other.start; - if (other.title != null) { - title = other.title; - } - if (other.link != null) { - link = other.link; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java b/app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java deleted file mode 100644 index 59844ae1f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/feed/VorbisCommentChapter.java +++ /dev/null @@ -1,109 +0,0 @@ -package de.danoeh.antennapod.feed; - -import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException; - -import java.util.concurrent.TimeUnit; - -public class VorbisCommentChapter extends Chapter { - public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3; - - private static final int CHAPTERXXX_LENGTH = "chapterxxx".length(); - - private int vorbisCommentId; - - public VorbisCommentChapter(int vorbisCommentId) { - this.vorbisCommentId = vorbisCommentId; - } - - public VorbisCommentChapter(long start, String title, FeedItem item, - String link) { - super(start, title, item, link); - } - - @Override - public String toString() { - return "VorbisCommentChapter [id=" + id + ", title=" + title - + ", link=" + link + ", start=" + start + "]"; - } - - public static long getStartTimeFromValue(String value) - throws VorbisCommentReaderException { - String[] parts = value.split(":"); - if (parts.length >= 3) { - try { - long hours = TimeUnit.MILLISECONDS.convert( - Long.parseLong(parts[0]), TimeUnit.HOURS); - long minutes = TimeUnit.MILLISECONDS.convert( - Long.parseLong(parts[1]), TimeUnit.MINUTES); - if (parts[2].contains("-->")) { - parts[2] = parts[2].substring(0, parts[2].indexOf("-->")); - } - long seconds = TimeUnit.MILLISECONDS.convert( - ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS); - return hours + minutes + seconds; - } catch (NumberFormatException e) { - throw new VorbisCommentReaderException(e); - } - } else { - throw new VorbisCommentReaderException("Invalid time string"); - } - } - - /** - * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx* - * - * @return the id of the chapter key or -1 if the id couldn't be read. - * @throws VorbisCommentReaderException - * */ - public static int getIDFromKey(String key) - throws VorbisCommentReaderException { - if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx - try { - String strId = key.substring(8, 10); - return Integer.parseInt(strId); - } catch (NumberFormatException e) { - throw new VorbisCommentReaderException(e); - } - } - throw new VorbisCommentReaderException("key is too short (" + key + ")"); - } - - /** - * Get the string that comes after 'CHAPTERxxx', for example 'name' or - * 'url'. - */ - public static String getAttributeTypeFromKey(String key) { - if (key.length() > CHAPTERXXX_LENGTH) { - return key.substring(CHAPTERXXX_LENGTH, key.length()); - } - return null; - } - - @Override - public int getChapterType() { - return CHAPTERTYPE_VORBISCOMMENT_CHAPTER; - } - - public void setTitle(String title) { - this.title = title; - } - - public void setLink(String link) { - this.link = link; - } - - public void setStart(long start) { - this.start = start; - } - - public int getVorbisCommentId() { - return vorbisCommentId; - } - - public void setVorbisCommentId(int vorbisCommentId) { - this.vorbisCommentId = vorbisCommentId; - } - - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index 082fe93fc..21e4cbd80 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -9,11 +9,11 @@ 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 de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index ffce518bf..69bd2b099 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -12,8 +12,8 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.util.playback.Playable; /** * Displays the cover and the title of a FeedItem. diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index d81ba4b86..9c7fade67 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -6,9 +6,9 @@ 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 de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.DBReader; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index 985673dd3..99320cffa 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -13,11 +13,11 @@ 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; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; /** * Fragment which is supposed to be displayed outside of the MediaplayerActivity diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 04c7fbf8e..f0e73a372 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -28,15 +28,15 @@ 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; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.ShownotesProvider; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.core.util.playback.Timeline; /** * Displays the description of a Playable object in a Webview. diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java index 909774467..b16e4f930 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -32,25 +32,25 @@ 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.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.asynctask.FeedRemover; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.dialog.FeedItemDialog; -import de.danoeh.antennapod.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; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.menuhandler.FeedMenuHandler; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; /** * Displays a list of FeedItems. diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index fe995256b..4f37f4613 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -19,22 +19,22 @@ 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.core.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 de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; import java.util.List; import java.util.concurrent.atomic.AtomicReference; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index 470186180..61e4ae1bb 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -16,17 +16,17 @@ 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.core.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 de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; import java.util.List; import java.util.concurrent.atomic.AtomicReference; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 2f322f75b..c1191d933 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -28,17 +28,17 @@ 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.core.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 de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; /** * Shows all items in the queue diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java index 89c30e34b..514b05efd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java @@ -5,9 +5,9 @@ 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 de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.storage.DownloadRequester; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java index b3ade4d70..23cc1d0b8 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -16,12 +16,12 @@ 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 de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.FeedSearcher; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; import java.util.List; 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 index 1b4616207..14b3a9c40 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -5,23 +5,21 @@ 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 de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; import java.util.List; 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 index 5717a74e7..33a35fa90 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java @@ -1,8 +1,8 @@ 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.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; import java.util.List; 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 index 801024787..b099953a8 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java @@ -10,11 +10,11 @@ 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; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; /** * Performs a search on the gpodder.net directory and displays the results. 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 index 45fe25580..133bb0281 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java @@ -1,9 +1,9 @@ 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 de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import java.util.ArrayList; import java.util.List; 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 index 204dda992..7e02b647f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java @@ -5,10 +5,10 @@ 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 de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; import java.util.List; 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 index a7e1033df..819a28c2d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -18,11 +18,11 @@ 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; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; public class TagListFragment extends ListFragment { private static final String TAG = "TagListFragment"; diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetService.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetService.java deleted file mode 100644 index 038b2a367..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetService.java +++ /dev/null @@ -1,718 +0,0 @@ -package de.danoeh.antennapod.gpoddernet; - -import org.apache.commons.lang3.Validate; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.auth.AuthenticationException; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.auth.BasicScheme; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; - -import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice; -import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; -import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange; -import de.danoeh.antennapod.gpoddernet.model.GpodnetTag; -import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse; -import de.danoeh.antennapod.preferences.GpodnetPreferences; -import de.danoeh.antennapod.service.download.AntennapodHttpClient; - -/** - * Communicates with the gpodder.net service. - */ -public class GpodnetService { - - private static final String BASE_SCHEME = "https"; - - public static final String DEFAULT_BASE_HOST = "gpodder.net"; - private final String BASE_HOST; - - private final HttpClient httpClient; - - public GpodnetService() { - httpClient = AntennapodHttpClient.getHttpClient(); - BASE_HOST = GpodnetPreferences.getHostname(); - } - - /** - * Returns the [count] most used tags. - */ - public List 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 deleted file mode 100644 index 3b0140826..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.gpoddernet; - -public class GpodnetServiceAuthenticationException extends GpodnetServiceException { - - public GpodnetServiceAuthenticationException() { - super(); - } - - public GpodnetServiceAuthenticationException(String message, Throwable cause) { - super(message, cause); - } - - public GpodnetServiceAuthenticationException(String message) { - super(message); - } - - public GpodnetServiceAuthenticationException(Throwable cause) { - super(cause); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java deleted file mode 100644 index a32e9357b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.danoeh.antennapod.gpoddernet; - -public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException { - int statusCode; - - public GpodnetServiceBadStatusCodeException(String message, int statusCode) { - super(message); - this.statusCode = statusCode; - } - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java deleted file mode 100644 index bdb394454..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.gpoddernet; - -public class GpodnetServiceException extends Exception { - - public GpodnetServiceException() { - } - - public GpodnetServiceException(String message) { - super(message); - } - - public GpodnetServiceException(Throwable cause) { - super(cause); - } - - public GpodnetServiceException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java deleted file mode 100644 index 86a2171fa..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java +++ /dev/null @@ -1,72 +0,0 @@ -package de.danoeh.antennapod.gpoddernet.model; - -import org.apache.commons.lang3.Validate; - -public class GpodnetDevice { - - private String id; - private String caption; - private DeviceType type; - private int subscriptions; - - public GpodnetDevice(String id, String caption, String type, - int subscriptions) { - Validate.notNull(id); - - this.id = id; - this.caption = caption; - this.type = DeviceType.fromString(type); - this.subscriptions = subscriptions; - } - - @Override - public String toString() { - return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type=" - + type + ", subscriptions=" + subscriptions + "]"; - } - - public static enum DeviceType { - DESKTOP, LAPTOP, MOBILE, SERVER, OTHER; - - static DeviceType fromString(String s) { - if (s == null) { - return OTHER; - } - - if (s.equals("desktop")) { - return DESKTOP; - } else if (s.equals("laptop")) { - return LAPTOP; - } else if (s.equals("mobile")) { - return MOBILE; - } else if (s.equals("server")) { - return SERVER; - } else { - return OTHER; - } - } - - @Override - public String toString() { - return super.toString().toLowerCase(); - } - - } - - public String getId() { - return id; - } - - public String getCaption() { - return caption; - } - - public DeviceType getType() { - return type; - } - - public int getSubscriptions() { - return subscriptions; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java deleted file mode 100644 index b002035c9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.danoeh.antennapod.gpoddernet.model; - -import org.apache.commons.lang3.Validate; - -public class GpodnetPodcast { - private String url; - private String title; - private String description; - private int subscribers; - private String logoUrl; - private String website; - private String mygpoLink; - - public GpodnetPodcast(String url, String title, String description, - int subscribers, String logoUrl, String website, String mygpoLink) { - Validate.notNull(url); - Validate.notNull(title); - Validate.notNull(description); - - this.url = url; - this.title = title; - this.description = description; - this.subscribers = subscribers; - this.logoUrl = logoUrl; - this.website = website; - this.mygpoLink = mygpoLink; - } - - @Override - public String toString() { - return "GpodnetPodcast [url=" + url + ", title=" + title - + ", description=" + description + ", subscribers=" - + subscribers + ", logoUrl=" + logoUrl + ", website=" + website - + ", mygpoLink=" + mygpoLink + "]"; - } - - public String getUrl() { - return url; - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public int getSubscribers() { - return subscribers; - } - - public String getLogoUrl() { - return logoUrl; - } - - public String getWebsite() { - return website; - } - - public String getMygpoLink() { - return mygpoLink; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java b/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java deleted file mode 100644 index a4617118d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.danoeh.antennapod.gpoddernet.model; - -import org.apache.commons.lang3.Validate; - -import java.util.List; - -public class GpodnetSubscriptionChange { - private List 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 deleted file mode 100644 index 80b84095e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.danoeh.antennapod.gpoddernet.model; - -import org.apache.commons.lang3.Validate; - -import java.util.Comparator; - -public class GpodnetTag { - - private String name; - private int usage; - - public GpodnetTag(String name, int usage) { - Validate.notNull(name); - - this.name = name; - this.usage = usage; - } - - public GpodnetTag(String name) { - super(); - this.name = name; - } - - @Override - public String toString() { - return "GpodnetTag [name=" + name + ", usage=" + usage + "]"; - } - - public String getName() { - return name; - } - - public int getUsage() { - return usage; - } - - public static class UsageComparator implements Comparator { - - @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 deleted file mode 100644 index fee8c7d28..000000000 --- a/app/src/main/java/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.gpoddernet.model; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; -import java.util.Map; - -/** - * Object returned by {@link de.danoeh.antennapod.gpoddernet.GpodnetService} in uploadChanges method. - */ -public class GpodnetUploadChangesResponse { - - /** - * timestamp/ID that can be used for requesting changes since this upload. - */ - public final long timestamp; - - /** - * URLs that should be updated. The key of the map is the original URL, the value of the map - * is the sanitized URL. - */ - public final Map 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 deleted file mode 100644 index 4cb563c04..000000000 --- a/app/src/main/java/de/danoeh/antennapod/opml/OpmlElement.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.danoeh.antennapod.opml; - -/** Represents a single feed in an OPML file. */ -public class OpmlElement { - private String text; - private String xmlUrl; - private String htmlUrl; - private String type; - - public OpmlElement() { - - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public String getXmlUrl() { - return xmlUrl; - } - - public void setXmlUrl(String xmlUrl) { - this.xmlUrl = xmlUrl; - } - - public String getHtmlUrl() { - return htmlUrl; - } - - public void setHtmlUrl(String htmlUrl) { - this.htmlUrl = htmlUrl; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java b/app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java deleted file mode 100644 index 19a980dee..000000000 --- a/app/src/main/java/de/danoeh/antennapod/opml/OpmlReader.java +++ /dev/null @@ -1,87 +0,0 @@ -package de.danoeh.antennapod.opml; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; - -/** Reads OPML documents. */ -public class OpmlReader { - private static final String TAG = "OpmlReader"; - - // ATTRIBUTES - private boolean isInOpml = false; - private ArrayList 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 deleted file mode 100644 index 4b0b7316a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/opml/OpmlSymbols.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.opml; - -/** Contains symbols for reading and writing OPML documents. */ -public final class OpmlSymbols { - - public static final String OPML = "opml"; - public static final String BODY = "body"; - public static final String OUTLINE = "outline"; - public static final String TEXT = "text"; - public static final String XMLURL = "xmlUrl"; - public static final String HTMLURL = "htmlUrl"; - public static final String TYPE = "type"; - public static final String VERSION = "version"; - public static final String HEAD = "head"; - public static final String TITLE = "title"; - - private OpmlSymbols() { - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java b/app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java deleted file mode 100644 index 405a5e35a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/opml/OpmlWriter.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.danoeh.antennapod.opml; - -import android.util.Log; -import android.util.Xml; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Feed; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; -import java.io.Writer; -import java.util.List; - -/** Writes OPML documents. */ -public class OpmlWriter { - private static final String TAG = "OpmlWriter"; - private static final String ENCODING = "UTF-8"; - private static final String OPML_VERSION = "2.0"; - private static final String OPML_TITLE = "AntennaPod Subscriptions"; - - /** - * Takes a list of feeds and a writer and writes those into an OPML - * document. - * - * @throws IOException - * @throws IllegalStateException - * @throws IllegalArgumentException - */ - public void writeDocument(List 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 deleted file mode 100644 index bdfe297a6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/preferences/GpodnetPreferences.java +++ /dev/null @@ -1,246 +0,0 @@ -package de.danoeh.antennapod.preferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.gpoddernet.GpodnetService; -import de.danoeh.antennapod.service.GpodnetSyncService; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.locks.ReentrantLock; - -/** - * Manages preferences for accessing gpodder.net service - */ -public class GpodnetPreferences { - - private static final String TAG = "GpodnetPreferences"; - - private static final String PREF_NAME = "gpodder.net"; - public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; - public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; - public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; - public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; - - - public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; - public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added"; - public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed"; - - private static String username; - private static String password; - private static String deviceID; - private static String hostname; - - private static ReentrantLock feedListLock = new ReentrantLock(); - private static Set 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 deleted file mode 100644 index 1d1ab052f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PlaybackPreferences.java +++ /dev/null @@ -1,146 +0,0 @@ -package de.danoeh.antennapod.preferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.BuildConfig; - -/** - * Provides access to preferences set by the playback service. A private - * instance of this class must first be instantiated via createInstance() or - * otherwise every public method will throw an Exception when called. - */ -public class PlaybackPreferences implements - SharedPreferences.OnSharedPreferenceChangeListener { - private static final String TAG = "PlaybackPreferences"; - - /** - * Contains the feed id of the currently playing item if it is a FeedMedia - * object. - */ - public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId"; - - /** - * Contains the id of the currently playing FeedMedia object or - * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object. - */ - public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId"; - - /** - * Type of the media object that is currently being played. This preference - * is set to NO_MEDIA_PLAYING after playback has been completed and is set - * as soon as the 'play' button is pressed. - */ - public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia"; - - /** True if last played media was streamed. */ - public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream"; - - /** True if last played media was a video. */ - public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo"; - - /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */ - public static final long NO_MEDIA_PLAYING = -1; - - private long currentlyPlayingFeedId; - private long currentlyPlayingFeedMediaId; - private long currentlyPlayingMedia; - private boolean currentEpisodeIsStream; - private boolean currentEpisodeIsVideo; - - private static PlaybackPreferences instance; - private Context context; - - private PlaybackPreferences(Context context) { - this.context = context; - loadPreferences(); - } - - /** - * Sets up the UserPreferences class. - * - * @throws IllegalArgumentException - * if context is null - * */ - public static void createInstance(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating new instance of UserPreferences"); - Validate.notNull(context); - - instance = new PlaybackPreferences(context); - - PreferenceManager.getDefaultSharedPreferences(context) - .registerOnSharedPreferenceChangeListener(instance); - } - - private void loadPreferences() { - SharedPreferences sp = PreferenceManager - .getDefaultSharedPreferences(context); - currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1); - currentlyPlayingFeedMediaId = sp.getLong( - PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); - currentlyPlayingMedia = sp.getLong(PREF_CURRENTLY_PLAYING_MEDIA, - NO_MEDIA_PLAYING); - currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); - currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - if (key.equals(PREF_CURRENTLY_PLAYING_FEED_ID)) { - currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, - -1); - - } else if (key.equals(PREF_CURRENTLY_PLAYING_MEDIA)) { - currentlyPlayingMedia = sp - .getLong(PREF_CURRENTLY_PLAYING_MEDIA, -1); - - } else if (key.equals(PREF_CURRENT_EPISODE_IS_STREAM)) { - currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); - - } else if (key.equals(PREF_CURRENT_EPISODE_IS_VIDEO)) { - currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); - - } else if (key.equals(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID)) { - currentlyPlayingFeedMediaId = sp.getLong( - PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); - } - } - - private static void instanceAvailable() { - if (instance == null) { - throw new IllegalStateException( - "UserPreferences was used before being set up"); - } - } - - - public static long getLastPlayedFeedId() { - instanceAvailable(); - return instance.currentlyPlayingFeedId; - } - - public static long getCurrentlyPlayingMedia() { - instanceAvailable(); - return instance.currentlyPlayingMedia; - } - - public static long getCurrentlyPlayingFeedMediaId() { - return instance.currentlyPlayingFeedMediaId; - } - - public static boolean getCurrentEpisodeIsStream() { - instanceAvailable(); - return instance.currentEpisodeIsStream; - } - - public static boolean getCurrentEpisodeIsVideo() { - instanceAvailable(); - return instance.currentEpisodeIsVideo; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java b/app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java deleted file mode 100644 index d7807ddac..000000000 --- a/app/src/main/java/de/danoeh/antennapod/preferences/UserPreferences.java +++ /dev/null @@ -1,577 +0,0 @@ -package de.danoeh.antennapod.preferences; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.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.receiver.FeedUpdateReceiver; - -/** - * Provides access to preferences set by the user in the settings screen. A - * private instance of this class must first be instantiated via - * createInstance() or otherwise every public method will throw an Exception - * when called. - */ -public class UserPreferences implements - SharedPreferences.OnSharedPreferenceChangeListener { - public static final String IMPORT_DIR = "import/"; - private static final String TAG = "UserPreferences"; - - public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; - public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; - public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; - public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; - public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; - public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; - public static final String PREF_AUTO_DELETE = "prefAutoDelete"; - public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; - public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; - public static final String PREF_THEME = "prefTheme"; - public static final String PREF_DATA_FOLDER = "prefDataFolder"; - public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; - public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter"; - private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; - public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; - private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; - private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; - public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; - private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; - - // 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, - 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 deleted file mode 100644 index a0539e276..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/AlarmUpdateReceiver.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import org.apache.commons.lang3.StringUtils; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.preferences.UserPreferences; - -/** Listens for events that make it necessary to reset the update alarm. */ -public class AlarmUpdateReceiver extends BroadcastReceiver { - private static final String TAG = "AlarmUpdateReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received intent"); - if (StringUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting update alarm after reboot"); - } else if (StringUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting update alarm after app upgrade"); - } - - UserPreferences.restartUpdateAlarm(UserPreferences.getUpdateInterval()); - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java deleted file mode 100644 index 4dcf0b6aa..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.util.Log; - -import org.apache.commons.lang3.StringUtils; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.util.NetworkUtils; - -public class ConnectivityActionReceiver extends BroadcastReceiver { - private static final String TAG = "ConnectivityActionReceiver"; - - @Override - public void onReceive(final Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received intent"); - - if (NetworkUtils.autodownloadNetworkAvailable(context)) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "auto-dl network available, starting auto-download"); - DBTasks.autodownloadUndownloadedItems(context); - } else { // if new network is Wi-Fi, finish ongoing downloads, - // otherwise cancel all downloads - ConnectivityManager cm = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo ni = cm.getActiveNetworkInfo(); - if (ni == null || ni.getType() != ConnectivityManager.TYPE_WIFI) { - if (BuildConfig.DEBUG) - Log.i(TAG, - "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads"); - DownloadRequester.getInstance().cancelAllDownloads(context); - } - - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java deleted file mode 100644 index 3c283a30b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.util.Log; - -import org.apache.commons.lang3.StringUtils; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.storage.DBTasks; - -/** Refreshes all feeds when it receives an intent */ -public class FeedUpdateReceiver extends BroadcastReceiver { - private static final String TAG = "FeedUpdateReceiver"; - public static final String ACTION_REFRESH_FEEDS = "de.danoeh.antennapod.feedupdatereceiver.refreshFeeds"; - - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_REFRESH_FEEDS)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received intent"); - boolean mobileUpdate = UserPreferences.isAllowMobileUpdate(); - if (mobileUpdate || connectedToWifi(context)) { - DBTasks.refreshExpiredFeeds(context); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Blocking automatic update: no wifi available / no mobile updates allowed"); - } - } - } - - private boolean connectedToWifi(Context context) { - ConnectivityManager connManager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo mWifi = connManager - .getNetworkInfo(ConnectivityManager.TYPE_WIFI); - - return mWifi.isConnected(); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java deleted file mode 100644 index 1edebd275..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/MediaButtonReceiver.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import android.view.KeyEvent; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.service.playback.PlaybackService; - -/** Receives media button events. */ -public class MediaButtonReceiver extends BroadcastReceiver { - private static final String TAG = "MediaButtonReceiver"; - public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.service.extra.MediaButtonReceiver.KEYCODE"; - - public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"; - - @Override - public void onReceive(Context context, Intent intent) { - if (BuildConfig.DEBUG) Log.d(TAG, "Received intent"); - KeyEvent event = (KeyEvent) intent.getExtras().get( - Intent.EXTRA_KEY_EVENT); - if (event.getAction() == KeyEvent.ACTION_DOWN) { - Intent serviceIntent = new Intent(context, PlaybackService.class); - int keycode = event.getKeyCode(); - serviceIntent.putExtra(EXTRA_KEYCODE, keycode); - context.startService(serviceIntent); - } - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java deleted file mode 100644 index 9f8892181..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import org.apache.commons.lang3.StringUtils; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.service.playback.PlayerWidgetService; - -public class PlayerWidget extends AppWidgetProvider { - private static final String TAG = "PlayerWidget"; - public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE"; - public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE"; - - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), FORCE_WIDGET_UPDATE)) { - startUpdate(context); - } else if (StringUtils.equals(intent.getAction(), STOP_WIDGET_UPDATE)) { - stopUpdate(context); - } - - } - - @Override - public void onEnabled(Context context) { - super.onEnabled(context); - if (BuildConfig.DEBUG) - Log.d(TAG, "Widget enabled"); - } - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, - int[] appWidgetIds) { - startUpdate(context); - } - - private void startUpdate(Context context) { - context.startService(new Intent(context, PlayerWidgetService.class)); - } - - private void stopUpdate(Context context) { - context.stopService(new Intent(context, PlayerWidgetService.class)); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java index b0430d170..359a546f6 100644 --- a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java +++ b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java @@ -7,9 +7,9 @@ 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 de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; diff --git a/app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java b/app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java deleted file mode 100644 index efc44c66a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/GpodnetSyncService.java +++ /dev/null @@ -1,245 +0,0 @@ -package de.danoeh.antennapod.service; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.gpoddernet.GpodnetService; -import de.danoeh.antennapod.gpoddernet.GpodnetServiceAuthenticationException; -import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange; -import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse; -import de.danoeh.antennapod.preferences.GpodnetPreferences; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.util.NetworkUtils; - -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument. - * This class also provides static methods for starting the GpodnetSyncService. - */ -public class GpodnetSyncService extends Service { - private static final String TAG = "GpodnetSyncService"; - - private static final long WAIT_INTERVAL = 5000L; - - public static final String ARG_ACTION = "action"; - - public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; - - private GpodnetService service; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; - if (action != null && action.equals(ACTION_SYNC)) { - Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); - syncWaiterThread.restart(); - } else { - Log.e(TAG, "Received invalid intent: action argument is null or invalid"); - } - return START_FLAG_REDELIVERY; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy"); - syncWaiterThread.interrupt(); - - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - private synchronized GpodnetService tryLogin() throws GpodnetServiceException { - if (service == null) { - service = new GpodnetService(); - service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); - } - return service; - } - - private synchronized void syncChanges() { - if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) { - final long timestamp = GpodnetPreferences.getLastSyncTimestamp(); - try { - final List 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); -// TODO getGpodnetSyncServiceErrorNotificationPendingIntent - 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 deleted file mode 100644 index ddf8d605d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/download/APRedirectHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.impl.client.DefaultRedirectHandler; -import org.apache.http.protocol.HttpContext; - -import java.net.URI; - -public class APRedirectHandler extends DefaultRedirectHandler { - // Identifier for logger - private static final String TAG = "APRedirectHandler"; - // Header field, which has to be potentially fixed - private static final String LOC = "Location"; - // Regular expressions for character strings, which should not appear in URLs - private static final String CHi[] = { "\\{", "\\}", "\\|", "\\\\", "\\^", "~", "\\[", "\\]", "\\`"}; - private static final String CHo[] = { "%7B", "%7D", "%7C", "%5C", "%5E", "%7E", "%5B", "%5D", "%60"}; - - /** - * Workaround for broken URLs in redirection. - * Proper solution involves LaxRedirectStrategy() which is not available in - * current API yet. - */ - @Override - public URI getLocationURI(HttpResponse response, HttpContext context) - throws org.apache.http.ProtocolException { - - Header h[] = response.getHeaders(LOC); - if (h.length>0) { - String s = h[0].getValue(); - - // Fix broken URL - for(int i=0; i 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 deleted file mode 100644 index d0d75d2d7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/download/DownloadService.java +++ /dev/null @@ -1,1231 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.MediaMetadataRetriever; -import android.os.Binder; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.webkit.URLUtil; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.apache.http.HttpStatus; -import org.xml.sax.SAXException; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletionService; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.xml.parsers.ParserConfigurationException; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.DownloadAuthenticationActivity; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.NavListAdapter; -import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.FeedPreferences; -import de.danoeh.antennapod.fragment.DownloadsFragment; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.syndication.handler.FeedHandler; -import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; -import de.danoeh.antennapod.util.ChapterUtils; -import de.danoeh.antennapod.util.DownloadError; -import de.danoeh.antennapod.util.InvalidFeedException; - -/** - * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. - * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of - * the intent. - * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the - * type of the feedfile. - */ -public class DownloadService extends Service { - private static final String TAG = "DownloadService"; - - /** - * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the - * object whose download should be cancelled. - */ - public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.service.cancelDownload"; - - /** - * Cancels all running downloads. - */ - public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.service.cancelAllDownloads"; - - /** - * Extra for ACTION_CANCEL_DOWNLOAD - */ - public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; - - /** - * Sent by the DownloadService when the content of the downloads list - * changes. - */ - public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.service.downloadsContentChanged"; - - /** - * Extra for ACTION_ENQUEUE_DOWNLOAD intent. - */ - public static final String EXTRA_REQUEST = "request"; - - /** - * Stores new media files that will be queued for auto-download if possible. - */ - private List 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() { // TODO getNotificationContentIntent - 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) { // TODO getReportNotificationContentIntent - 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(); - - // TODO getAuthentificationNotificationContentIntent - 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(); - DownloadRequester requester = DownloadRequester.getInstance(); - int tasks = 0; - - try { - DownloadRequest request = completedRequests.take(); - parserService.submit(new FeedParserTask(request)); - tasks++; - } catch (InterruptedException e) { - return null; - } - - tasks += pollCompletedDownloads(); - - isCollectingRequests = true; - - if (requester.isDownloadingFeeds()) { - // wait for completion of more downloads - long startTime = System.currentTimeMillis(); - long currentTime = startTime; - while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { - try { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); - sleep(startTime + WAIT_TIMEOUT - currentTime); - } catch (InterruptedException e) { - if (BuildConfig.DEBUG) - Log.d(TAG, "interrupted while waiting for more downloads"); - tasks += pollCompletedDownloads(); - } finally { - currentTime = System.currentTimeMillis(); - } - } - - tasks += pollCompletedDownloads(); - - } - - isCollectingRequests = false; - - for (int i = 0; i < tasks; i++) { - try { - Feed f = parserService.take().get(); - if (f != null) { - results.add(f); - } - } catch (InterruptedException e) { - e.printStackTrace(); - - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - return results; - } - - private int pollCompletedDownloads() { - int tasks = 0; - for (int i = 0; i < completedRequests.size(); i++) { - parserService.submit(new FeedParserTask(completedRequests.poll())); - tasks++; - } - return tasks; - } - - @Override - public void run() { - while (isActive) { - final List feeds = collectCompletedRequests(); - - if (feeds == null) { - continue; - } - - if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + feeds.size() + " feeds"); - - for (Feed feed : feeds) { - removeDuplicateImages(feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. - } - - // Save information of feed in DB - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - dbUpdateFuture = dbService.submit(new Runnable() { - @Override - public void run() { - Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, feeds.toArray(new Feed[feeds.size()])); - - for (Feed savedFeed : savedFeeds) { - // Download Feed Image if provided and not downloaded - if (savedFeed.getImage() != null - && savedFeed.getImage().isDownloaded() == false) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed has image; Downloading...."); - savedFeed.getImage().setOwner(savedFeed); - final Feed savedFeedRef = savedFeed; - try { - requester.downloadImage(DownloadService.this, - savedFeedRef.getImage()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - DownloadService.this, - new DownloadStatus( - savedFeedRef.getImage(), - savedFeedRef - .getImage() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage() - ) - ); - } - } - - // queue new media files for automatic download - for (FeedItem item : savedFeed.getItems()) { - if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { - newMediaFiles.add(item.getMedia().getId()); - } - } - - numberOfDownloads.decrementAndGet(); - } - - sendDownloadHandledIntent(); - - queryDownloadsAsync(); - } - }); - - } - - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down"); - - } - - private class FeedParserTask implements Callable { - - private DownloadRequest request; - - private FeedParserTask(DownloadRequest request) { - this.request = request; - } - - @Override - public Feed call() throws Exception { - return parseFeed(request); - } - } - - private Feed parseFeed(DownloadRequest request) { - Feed savedFeed = null; - - Feed feed = new Feed(request.getSource(), new Date()); - feed.setFile_url(request.getDestination()); - feed.setId(request.getFeedfileId()); - feed.setDownloaded(true); - feed.setPreferences(new FeedPreferences(0, true, request.getUsername(), request.getPassword())); - - DownloadError reason = null; - String reasonDetailed = null; - boolean successful = true; - FeedHandler feedHandler = new FeedHandler(); - - try { - feed = feedHandler.parseFeed(feed).feed; - if (BuildConfig.DEBUG) - Log.d(TAG, feed.getTitle() + " parsed"); - if (checkFeedData(feed) == false) { - throw new InvalidFeedException(); - } - - } catch (SAXException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (IOException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (ParserConfigurationException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_UNSUPPORTED_TYPE; - reasonDetailed = e.getMessage(); - } catch (InvalidFeedException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } - - // cleanup(); - if (savedFeed == null) { - savedFeed = feed; - } - - - if (successful) { - return savedFeed; - } else { - numberOfDownloads.decrementAndGet(); - saveDownloadStatus(new DownloadStatus(savedFeed, - savedFeed.getHumanReadableIdentifier(), reason, successful, - reasonDetailed)); - return null; - } - } - - - /** - * Checks if the feed was parsed correctly. - */ - private boolean checkFeedData(Feed feed) { - if (feed.getTitle() == null) { - Log.e(TAG, "Feed has no title."); - return false; - } - if (!hasValidFeedItems(feed)) { - Log.e(TAG, "Feed has invalid items"); - return false; - } - return true; - } - - /** - * Checks if the FeedItems of this feed have images that point - * to the same URL. If two FeedItems have an image that points to - * the same URL, the reference of the second item is removed, so that every image - * reference is unique. - */ - private void removeDuplicateImages(Feed feed) { - for (int x = 0; x < feed.getItems().size(); x++) { - for (int y = x + 1; y < feed.getItems().size(); y++) { - FeedItem item1 = feed.getItems().get(x); - FeedItem item2 = feed.getItems().get(y); - if (item1.hasItemImage() && item2.hasItemImage()) { - if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) { - item2.setImage(null); - } - } - } - } - } - - private boolean hasValidFeedItems(Feed feed) { - for (FeedItem item : feed.getItems()) { - if (item.getTitle() == null) { - Log.e(TAG, "Item has no title"); - return false; - } - if (item.getPubDate() == null) { - Log.e(TAG, - "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } - } - return true; - } - - /** - * Delete files that aren't needed anymore - */ - private void cleanup(Feed feed) { - if (feed.getFile_url() != null) { - if (new File(feed.getFile_url()).delete()) - if (BuildConfig.DEBUG) - Log.d(TAG, "Successfully deleted cache file."); - else - Log.e(TAG, "Failed to delete cache file."); - feed.setFile_url(null); - } else if (BuildConfig.DEBUG) { - Log.d(TAG, "Didn't delete cache file: File url is not set."); - } - } - - public void shutdown() { - isActive = false; - if (isCollectingRequests) { - interrupt(); - } - } - - public void submitCompletedDownload(DownloadRequest request) { - completedRequests.offer(request); - if (isCollectingRequests) { - interrupt(); - } - } - - } - - /** - * Handles failed downloads. - *

- * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location - * of the downloaded file. - *

- * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. - */ - class FailedDownloadHandler implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - FailedDownloadHandler(DownloadStatus status, DownloadRequest request) { - this.request = request; - this.status = status; - } - - @Override - public void run() { - if (request.isDeleteOnFailure()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); - } else { - File dest = new File(request.getDestination()); - if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - Log.d(TAG, "File has been partially downloaded. Writing file url"); - FeedMedia media = DBReader.getFeedMedia(DownloadService.this, request.getFeedfileId()); - media.setFile_url(request.getDestination()); - try { - DBWriter.setFeedMedia(DownloadService.this, media).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - } - } - } - - /** - * Handles a completed image download. - */ - class ImageHandlerThread implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { - Validate.notNull(status); - Validate.notNull(request); - - this.status = status; - this.request = request; - } - - @Override - public void run() { - FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId()); - if (image == null) { - throw new IllegalStateException("Could not find downloaded image in database"); - } - - image.setFile_url(request.getDestination()); - image.setDownloaded(true); - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - DBWriter.setFeedImage(DownloadService.this, image); - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - } - } - - /** - * Handles a completed media download. - */ - class MediaHandlerThread implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { - Validate.notNull(status); - Validate.notNull(request); - - this.status = status; - this.request = request; - } - - @Override - public void run() { - FeedMedia media = DBReader.getFeedMedia(DownloadService.this, - request.getFeedfileId()); - if (media == null) { - throw new IllegalStateException( - "Could not find downloaded media object in database"); - } - boolean chaptersRead = false; - media.setDownloaded(true); - media.setFile_url(request.getDestination()); - - // Get duration - MediaMetadataRetriever mmr = null; - try { - mmr = new MediaMetadataRetriever(); - mmr.setDataSource(media.getFile_url()); - String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - media.setDuration(Integer.parseInt(durationStr)); - if (BuildConfig.DEBUG) - Log.d(TAG, "Duration of file is " + media.getDuration()); - } catch (NumberFormatException e) { - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } finally { - if (mmr != null) { - mmr.release(); - } - } - - if (media.getItem().getChapters() == null) { - ChapterUtils.loadChaptersFromFileUrl(media); - if (media.getItem().getChapters() != null) { - chaptersRead = true; - } - } - - try { - if (chaptersRead) { - DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); - } - DBWriter.setFeedMedia(DownloadService.this, media).get(); - if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { - DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get(); - } - } catch (ExecutionException e) { - e.printStackTrace(); - status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); - } catch (InterruptedException e) { - e.printStackTrace(); - status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); - } - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - } - } - - /** - * Schedules the notification updater task if it hasn't been scheduled yet. - */ - private void setupNotificationUpdater() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting up notification updater"); - if (notificationUpdater == null) { - notificationUpdater = new NotificationUpdater(); - notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( - notificationUpdater, 5L, 5L, TimeUnit.SECONDS); - } - } - - private void cancelNotificationUpdater() { - boolean result = false; - if (notificationUpdaterFuture != null) { - result = notificationUpdaterFuture.cancel(true); - } - notificationUpdater = null; - notificationUpdaterFuture = null; - Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); - } - - private class NotificationUpdater implements Runnable { - public void run() { - handler.post(new Runnable() { - @Override - public void run() { - Notification n = updateNotifications(); - if (n != null) { - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, n); - } - } - }); - } - } - - public List getDownloads() { - return downloads; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/service/download/DownloadStatus.java b/app/src/main/java/de/danoeh/antennapod/service/download/DownloadStatus.java deleted file mode 100644 index 1d76770bb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/download/DownloadStatus.java +++ /dev/null @@ -1,181 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.feed.FeedFile; -import de.danoeh.antennapod.util.DownloadError; - -import java.util.Date; - -/** Contains status attributes for one download */ -public class DownloadStatus { - /** - * Downloaders should use this constant for the size attribute if necessary - * so that the listadapters etc. can react properly. - */ - public static final int SIZE_UNKNOWN = -1; - - // ----------------------------------- ATTRIBUTES STORED IN DB - /** Unique id for storing the object in database. */ - protected long id; - /** - * A human-readable string which is shown to the user so that he can - * identify the download. Should be the title of the item/feed/media or the - * URL if the download has no other title. - */ - protected String title; - protected DownloadError reason; - /** - * A message which can be presented to the user to give more information. - * Should be null if Download was successful. - */ - protected String reasonDetailed; - protected boolean successful; - protected Date completionDate; - protected long feedfileId; - /** - * Is used to determine the type of the feedfile even if the feedfile does - * not exist anymore. The value should be FEEDFILETYPE_FEED, - * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA - */ - protected int feedfileType; - - // ------------------------------------ NOT STORED IN DB - protected boolean done; - protected boolean cancelled; - - /** Constructor for restoring Download status entries from DB. */ - public DownloadStatus(long id, String title, long feedfileId, - int feedfileType, boolean successful, DownloadError reason, - Date completionDate, String reasonDetailed) { - this.id = id; - this.title = title; - this.done = true; - this.feedfileId = feedfileId; - this.reason = reason; - this.successful = successful; - this.completionDate = (Date) completionDate.clone(); - this.reasonDetailed = reasonDetailed; - this.feedfileType = feedfileType; - } - - public DownloadStatus(DownloadRequest request, DownloadError reason, - boolean successful, boolean cancelled, String reasonDetailed) { - Validate.notNull(request); - - this.title = request.getTitle(); - this.feedfileId = request.getFeedfileId(); - this.feedfileType = request.getFeedfileType(); - this.reason = reason; - this.successful = successful; - this.cancelled = cancelled; - this.reasonDetailed = reasonDetailed; - this.completionDate = new Date(); - } - - /** Constructor for creating new completed downloads. */ - public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, - boolean successful, String reasonDetailed) { - Validate.notNull(feedfile); - - this.title = title; - this.done = true; - this.feedfileId = feedfile.getId(); - this.feedfileType = feedfile.getTypeAsInt(); - this.reason = reason; - this.successful = successful; - this.completionDate = new Date(); - this.reasonDetailed = reasonDetailed; - } - - /** Constructor for creating new completed downloads. */ - public DownloadStatus(long feedfileId, int feedfileType, String title, - DownloadError reason, boolean successful, String reasonDetailed) { - this.title = title; - this.done = true; - this.feedfileId = feedfileId; - this.feedfileType = feedfileType; - this.reason = reason; - this.successful = successful; - this.completionDate = new Date(); - this.reasonDetailed = reasonDetailed; - } - - @Override - public String toString() { - return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" - + reason + ", reasonDetailed=" + reasonDetailed - + ", successful=" + successful + ", completionDate=" - + completionDate + ", feedfileId=" + feedfileId - + ", feedfileType=" + feedfileType + ", done=" + done - + ", cancelled=" + cancelled + "]"; - } - - public long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public DownloadError getReason() { - return reason; - } - - public String getReasonDetailed() { - return reasonDetailed; - } - - public boolean isSuccessful() { - return successful; - } - - public Date getCompletionDate() { - return (Date) completionDate.clone(); - } - - public long getFeedfileId() { - return feedfileId; - } - - public int getFeedfileType() { - return feedfileType; - } - - public boolean isDone() { - return done; - } - - public boolean isCancelled() { - return cancelled; - } - - public void setSuccessful() { - this.successful = true; - this.reason = DownloadError.SUCCESS; - this.done = true; - } - - public void setFailed(DownloadError reason, String reasonDetailed) { - this.successful = false; - this.reason = reason; - this.reasonDetailed = reasonDetailed; - this.done = true; - } - - public void setCancelled() { - this.successful = false; - this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; - this.done = true; - this.cancelled = true; - } - - public void setCompletionDate(Date completionDate) { - this.completionDate = (Date) completionDate.clone(); - } - - public void setId(long id) { - this.id = id; - } -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/service/download/Downloader.java b/app/src/main/java/de/danoeh/antennapod/service/download/Downloader.java deleted file mode 100644 index 80cc5b3f8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/download/Downloader.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.content.Context; -import android.net.wifi.WifiManager; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; - -import java.util.concurrent.Callable; - -/** Downloads files */ -public abstract class Downloader implements Callable { - private static final String TAG = "Downloader"; - - protected volatile boolean finished; - - protected volatile boolean cancelled; - - protected DownloadRequest request; - protected DownloadStatus result; - - public Downloader(DownloadRequest request) { - super(); - this.request = request; - this.request.setStatusMsg(R.string.download_pending); - this.cancelled = false; - this.result = new DownloadStatus(request, null, false, false, null); - } - - protected abstract void download(); - - public final Downloader call() { - WifiManager wifiManager = (WifiManager) PodcastApp.getInstance().getSystemService(Context.WIFI_SERVICE); - WifiManager.WifiLock wifiLock = null; - if (wifiManager != null) { - wifiLock = wifiManager.createWifiLock(TAG); - wifiLock.acquire(); - } - - download(); - - if (wifiLock != null) { - wifiLock.release(); - } - - if (result == null) { - throw new IllegalStateException( - "Downloader hasn't created DownloadStatus object"); - } - finished = true; - return this; - } - - public DownloadRequest getDownloadRequest() { - return request; - } - - public DownloadStatus getResult() { - return result; - } - - public boolean isFinished() { - return finished; - } - - public void cancel() { - cancelled = true; - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/service/download/DownloaderCallback.java b/app/src/main/java/de/danoeh/antennapod/service/download/DownloaderCallback.java deleted file mode 100644 index 08420e83a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/download/DownloaderCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.danoeh.antennapod.service.download; - -/** - * Callback used by the Downloader-classes to notify the requester that the - * download has completed. - */ -public interface DownloaderCallback { - - public void onDownloadCompleted(Downloader downloader); -} diff --git a/app/src/main/java/de/danoeh/antennapod/service/download/HttpDownloader.java b/app/src/main/java/de/danoeh/antennapod/service/download/HttpDownloader.java deleted file mode 100644 index 7ae96dc07..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/download/HttpDownloader.java +++ /dev/null @@ -1,246 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.net.http.AndroidHttpClient; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.util.DownloadError; -import de.danoeh.antennapod.util.StorageUtils; -import de.danoeh.antennapod.util.URIUtil; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.auth.BasicScheme; -import org.apache.http.message.BasicHeader; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; - -public class HttpDownloader extends Downloader { - private static final String TAG = "HttpDownloader"; - - private static final int BUFFER_SIZE = 8 * 1024; - - public HttpDownloader(DownloadRequest request) { - super(request); - } - - @Override - protected void download() { - File destination = new File(request.getDestination()); - final boolean fileExists = destination.exists(); - - if (request.isDeleteOnFailure() && fileExists) { - Log.w(TAG, "File already exists"); - if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { - onFail(DownloadError.ERROR_FILE_EXISTS, null); - return; - } else { - onSuccess(); - return; - } - } - - HttpClient httpClient = AntennapodHttpClient.getHttpClient(); - RandomAccessFile out = null; - InputStream connection = null; - try { - HttpGet httpGet = new HttpGet(URIUtil.getURIFromRequestUrl(request.getSource())); - - // add authentication information - String userInfo = httpGet.getURI().getUserInfo(); - if (userInfo != null) { - String[] parts = userInfo.split(":"); - if (parts.length == 2) { - httpGet.addHeader(BasicScheme.authenticate( - new UsernamePasswordCredentials(parts[0], parts[1]), - "UTF-8", false)); - } - } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { - httpGet.addHeader(BasicScheme.authenticate(new UsernamePasswordCredentials(request.getUsername(), - request.getPassword()), "UTF-8", false)); - } - - // add range header if necessary - if (fileExists) { - request.setSoFar(destination.length()); - httpGet.addHeader(new BasicHeader("Range", - "bytes=" + request.getSoFar() + "-")); - if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar()); - } - - HttpResponse response = httpClient.execute(httpGet); - HttpEntity httpEntity = response.getEntity(); - int responseCode = response.getStatusLine().getStatusCode(); - Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); - - final boolean isGzip = contentEncodingHeader != null && - contentEncodingHeader.getValue().equalsIgnoreCase("gzip"); - - if (BuildConfig.DEBUG) - Log.d(TAG, "Response code is " + responseCode); - - if (responseCode / 100 != 2 || httpEntity == null) { - final DownloadError error; - final String details; - if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - error = DownloadError.ERROR_UNAUTHORIZED; - details = String.valueOf(responseCode); - } else { - error = DownloadError.ERROR_HTTP_DATA_ERROR; - details = String.valueOf(responseCode); - } - onFail(error, details); - return; - } - - if (!StorageUtils.storageAvailable(PodcastApp.getInstance())) { - onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); - return; - } - - connection = new BufferedInputStream(AndroidHttpClient - .getUngzippedContent(httpEntity)); - - Header[] contentRangeHeaders = (fileExists) ? response.getHeaders("Content-Range") : null; - - if (fileExists && responseCode == HttpStatus.SC_PARTIAL_CONTENT - && contentRangeHeaders != null && contentRangeHeaders.length > 0) { - String start = contentRangeHeaders[0].getValue().substring("bytes ".length(), - contentRangeHeaders[0].getValue().indexOf("-")); - request.setSoFar(Long.valueOf(start)); - Log.d(TAG, "Starting download at position " + request.getSoFar()); - - out = new RandomAccessFile(destination, "rw"); - out.seek(request.getSoFar()); - } else { - destination.delete(); - destination.createNewFile(); - out = new RandomAccessFile(destination, "rw"); - } - - - byte[] buffer = new byte[BUFFER_SIZE]; - int count = 0; - request.setStatusMsg(R.string.download_running); - if (BuildConfig.DEBUG) - Log.d(TAG, "Getting size of download"); - request.setSize(httpEntity.getContentLength() + request.getSoFar()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Size is " + request.getSize()); - if (request.getSize() < 0) { - request.setSize(DownloadStatus.SIZE_UNKNOWN); - } - - long freeSpace = StorageUtils.getFreeSpaceAvailable(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Free space is " + freeSpace); - - if (request.getSize() != DownloadStatus.SIZE_UNKNOWN - && request.getSize() > freeSpace) { - onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); - return; - } - - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting download"); - while (!cancelled - && (count = connection.read(buffer)) != -1) { - out.write(buffer, 0, count); - request.setSoFar(request.getSoFar() + count); - request.setProgressPercent((int) (((double) request - .getSoFar() / (double) request - .getSize()) * 100)); - } - if (cancelled) { - onCancelled(); - } else { - // check if size specified in the response header is the same as the size of the - // written file. This check cannot be made if compression was used - if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && - request.getSoFar() != request.getSize()) { - onFail(DownloadError.ERROR_IO_ERROR, - "Download completed but size: " + - request.getSoFar() + - " does not equal expected size " + - request.getSize() - ); - return; - } - onSuccess(); - } - - } catch (IllegalArgumentException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); - } catch (SocketTimeoutException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); - } catch (UnknownHostException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); - } catch (IOException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); - } catch (NullPointerException e) { - // might be thrown by connection.getInputStream() - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); - } finally { - IOUtils.closeQuietly(out); - AntennapodHttpClient.cleanup(); - } - } - - private void onSuccess() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was successful"); - result.setSuccessful(); - } - - private void onFail(DownloadError reason, String reasonDetailed) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Download failed"); - } - result.setFailed(reason, reasonDetailed); - if (request.isDeleteOnFailure()) { - cleanup(); - } - } - - private void onCancelled() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was cancelled"); - result.setCancelled(); - cleanup(); - } - - /** - * Deletes unfinished downloads. - */ - private void cleanup() { - if (request.getDestination() != null) { - File dest = new File(request.getDestination()); - if (dest.exists()) { - boolean rc = dest.delete(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " - + rc); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "cleanup() didn't delete file: does not exist."); - } - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackService.java b/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackService.java deleted file mode 100644 index 96b9588c4..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackService.java +++ /dev/null @@ -1,1080 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.media.RemoteControlClient.MetadataEditor; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.util.Pair; -import android.view.KeyEvent; -import android.view.SurfaceHolder; -import android.widget.Toast; - -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.util.List; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AudioplayerActivity; -import de.danoeh.antennapod.activity.VideoplayerActivity; -import de.danoeh.antennapod.asynctask.PicassoProvider; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.receiver.PlayerWidget; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.flattr.FlattrUtils; -import de.danoeh.antennapod.util.playback.Playable; - -/** - * Controls the MediaPlayer that plays a FeedMedia-file - */ -public class PlaybackService extends Service { - /** - * Logging tag - */ - private static final String TAG = "PlaybackService"; - - /** - * Parcelable of type Playable. - */ - public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** - * True if media should be streamed. - */ - public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream"; - /** - * True if playback should be started immediately after media has been - * prepared. - */ - public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared"; - - public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; - - public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; - private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; - private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; - - public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification"; - public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode"; - public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType"; - - /** - * If the PlaybackService receives this action, it will stop playback and - * try to shutdown. - */ - public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; - - /** - * If the PlaybackService receives this action, it will end playback of the - * current episode and load the next episode if there is one available. - */ - public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; - - /** - * Used in NOTIFICATION_TYPE_RELOAD. - */ - public static final int EXTRA_CODE_AUDIO = 1; - public static final int EXTRA_CODE_VIDEO = 2; - - public static final int NOTIFICATION_TYPE_ERROR = 0; - public static final int NOTIFICATION_TYPE_INFO = 1; - public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; - - /** - * Receivers of this intent should update their information about the curently playing media - */ - public static final int NOTIFICATION_TYPE_RELOAD = 3; - /** - * The state of the sleeptimer changed. - */ - public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; - public static final int NOTIFICATION_TYPE_BUFFER_START = 5; - public static final int NOTIFICATION_TYPE_BUFFER_END = 6; - /** - * No more episodes are going to be played. - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; - - /** - * Playback speed has changed - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; - - /** - * Returned by getPositionSafe() or getDurationSafe() if the playbackService - * is in an invalid state. - */ - public static final int INVALID_TIME = -1; - - /** - * Is true if service is running. - */ - public static boolean isRunning = false; - /** - * Is true if service has received a valid start command. - */ - public static boolean started = false; - - private static final int NOTIFICATION_ID = 1; - - private RemoteControlClient remoteControlClient; - private PlaybackServiceMediaPlayer mediaPlayer; - private PlaybackServiceTaskManager taskManager; - - private static volatile MediaType currentMediaType = MediaType.UNKNOWN; - - private final IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - public PlaybackService getService() { - return PlaybackService.this; - } - } - - @Override - public boolean onUnbind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onUnbind event"); - return super.onUnbind(intent); - } - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - */ - public static Intent getPlayerActivityIntent(Context context) { - if (isRunning) { // TODO getPlayerActivityIntent - if (currentMediaType == MediaType.VIDEO) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } else { - if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - } - - /** - * Same as getPlayerActivityIntent(context), but here the type of activity - * depends on the FeedMedia that is provided as an argument. - */ - public static Intent getPlayerActivityIntent(Context context, Playable media) { - MediaType mt = media.getMediaType(); - if (mt == MediaType.VIDEO) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - super.onCreate(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service created."); - isRunning = true; - - registerReceiver(headsetDisconnected, new IntentFilter( - Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - registerReceiver(audioBecomingNoisy, new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( - ACTION_SKIP_CURRENT_EPISODE)); - remoteControlClient = setupRemoteControlClient(); - taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); - mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); - - } - - @SuppressLint("NewApi") - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); - isRunning = false; - started = false; - currentMediaType = MediaType.UNKNOWN; - - unregisterReceiver(headsetDisconnected); - unregisterReceiver(shutdownReceiver); - unregisterReceiver(audioBecomingNoisy); - unregisterReceiver(skipCurrentEpisodeReceiver); - mediaPlayer.shutdown(); - taskManager.shutdown(); - } - - @Override - public IBinder onBind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onBind event"); - return mBinder; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - if (BuildConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); - final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - if (keycode == -1 && playable == null) { - Log.e(TAG, "PlaybackService was started with no arguments"); - stopSelf(); - } - - if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); - stopForeground(true); - } else { - - if (keycode != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received media button event"); - handleKeycode(keycode); - } else { - started = true; - boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); - boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); - } - } - - return Service.START_REDELIVER_INTENT; - } - - /** - * Handles media button events - */ - private void handleKeycode(int keycode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling keycode: " + keycode); - - final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - final PlayerStatus status = info.playerStatus; - switch (keycode) { - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, true); - } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.PREPARING) { - mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, true); - } - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs()); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - case KeyEvent.KEYCODE_MEDIA_REWIND: - mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs()); - break; - default: - if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something - String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - break; - } - } - - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - public void setVideoSurface(SurfaceHolder sh) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting display"); - mediaPlayer.setVideoSurface(sh); - } - - /** - * Called when the surface holder of the mediaplayer has to be changed. - */ - private void resetVideoSurface() { - taskManager.cancelPositionSaver(); - mediaPlayer.resetVideoSurface(); - } - - public void notifyVideoSurfaceAbandoned() { - stopForeground(true); - mediaPlayer.resetVideoSurface(); - } - - private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { - @Override - public void positionSaverTick() { - saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); - } - - @Override - public void onSleepTimerExpired() { - mediaPlayer.pause(true, true); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - - @Override - public void onWidgetUpdaterTick() { - updateWidget(); - } - - @Override - public void onChapterLoaded(Playable media) { - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } - }; - - private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { - @Override - public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { - currentMediaType = mediaPlayer.getCurrentMediaType(); - switch (newInfo.playerStatus) { - case INITIALIZED: - writePlaybackPreferences(); - break; - - case PREPARED: - taskManager.startChapterLoader(newInfo.playable); - break; - - case PAUSED: - taskManager.cancelPositionSaver(); - saveCurrentPosition(false, 0); - taskManager.cancelWidgetUpdater(); - stopForeground(true); - break; - - case STOPPED: - //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - //stopSelf(); - break; - - case PLAYING: - if (BuildConfig.DEBUG) - Log.d(TAG, "Audiofocus successfully requested"); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resuming/Starting playback"); - - taskManager.startPositionSaver(); - taskManager.startWidgetUpdater(); - setupNotification(newInfo); - break; - case ERROR: - writePlaybackPreferencesNoMediaPlaying(); - break; - - } - - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - updateWidget(); - refreshRemoteControlClientState(newInfo); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); - } - - @Override - public void shouldStop() { - stopSelf(); - } - - @Override - public void playbackSpeedChanged(float s) { - sendNotificationBroadcast( - NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); - } - - @Override - public void onBufferingUpdate(int percent) { - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); - } - - @Override - public boolean onMediaPlayerInfo(int code) { - switch (code) { - case MediaPlayer.MEDIA_INFO_BUFFERING_START: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); - return true; - case MediaPlayer.MEDIA_INFO_BUFFERING_END: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); - return true; - default: - return false; - } - } - - @Override - public boolean onMediaPlayerError(Object inObj, int what, int extra) { - final String TAG = "PlaybackService.onErrorListener"; - Log.w(TAG, "An error has occured: " + what + " " + extra); - if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, false); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); - writePlaybackPreferencesNoMediaPlaying(); - stopSelf(); - return true; - } - - @Override - public boolean endPlayback(boolean playNextEpisode) { - PlaybackService.this.endPlayback(true); - return true; - } - - @Override - public RemoteControlClient getRemoteControlClient() { - return remoteControlClient; - } - }; - - private void endPlayback(boolean playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback ended"); - - final Playable media = mediaPlayer.getPSMPInfo().playable; - if (media == null) { - Log.e(TAG, "Cannot end playback: media was null"); - return; - } - - taskManager.cancelPositionSaver(); - - boolean isInQueue = false; - FeedItem nextItem = null; - - if (media instanceof FeedMedia) { - FeedItem item = ((FeedMedia) media).getItem(); - DBWriter.markItemRead(PlaybackService.this, item, true, true); - - try { - final List queue = taskManager.getQueue(); - isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); - nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); - } catch (InterruptedException e) { - e.printStackTrace(); - // isInQueue remains false - } - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); - } - DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); - - // auto-flattr if enabled - if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { - DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); - } - } - - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - Playable nextMedia = null; - boolean loadNextItem = isInQueue && nextItem != null; - playNextEpisode = playNextEpisode && loadNextItem - && UserPreferences.isFollowQueue(); - if (loadNextItem) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading next item in queue"); - nextMedia = nextItem.getMedia(); - } - final boolean prepareImmediately; - final boolean startWhenPrepared; - final boolean stream; - - if (playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No more episodes available to play"); - - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - writePlaybackPreferencesNoMediaPlaying(); - if (nextMedia != null) { - stream = !media.localFileAvailable(); - mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - mediaPlayer.stop(); - //stopSelf(); - } - } - - public void setSleepTimer(long waitingTime) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); - taskManager.setSleepTimer(waitingTime); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - public void disableSleepTimer() { - taskManager.disableSleepTimer(); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - private void writePlaybackPreferencesNoMediaPlaying() { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.commit(); - } - - - private void writePlaybackPreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Writing playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - MediaType mediaType = mediaPlayer.getCurrentMediaType(); - boolean stream = mediaPlayer.isStreaming(); - - if (info.playable != null) { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - info.playable.getPlayableType()); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - stream); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, - mediaType == MediaType.VIDEO); - if (info.playable instanceof FeedMedia) { - FeedMedia fMedia = (FeedMedia) info.playable; - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - fMedia.getItem().getFeed().getId()); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - fMedia.getId()); - } else { - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - info.playable.writeToPreferences(editor); - } else { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - - editor.commit(); - } - - /** - * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. - */ - private void postStatusUpdateIntent() { - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - } - - private void sendNotificationBroadcast(int type, int code) { - Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); - intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); - intent.putExtra(EXTRA_NOTIFICATION_CODE, code); - sendBroadcast(intent); - } - - /** - * Used by setupNotification to load notification data in another thread. - */ - private AsyncTask notificationSetupTask; - - /** - * Prepares notification and starts the service in the foreground. - */ - @SuppressLint("NewApi") - private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { - final PendingIntent pIntent = PendingIntent.getActivity(this, 0, - PlaybackService.getPlayerActivityIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); - - if (notificationSetupTask != null) { - notificationSetupTask.cancel(true); - } - notificationSetupTask = new AsyncTask() { - Bitmap icon = null; - - @Override - protected Void doInBackground(Void... params) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting background work"); - if (android.os.Build.VERSION.SDK_INT >= 11) { - if (info.playable != null) { - try { - int iconSize = getResources().getDimensionPixelSize( - android.R.dimen.notification_large_icon_width); - icon = PicassoProvider.getMediaMetadataPicassoInstance(PlaybackService.this) - .load(info.playable.getImageUri()) - .resize(iconSize, iconSize) - .get(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - } - if (icon == null) { - icon = BitmapFactory.decodeResource(getResources(), - R.drawable.ic_stat_antenna); - } - - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING - && info.playable != null) { - String contentText = info.playable.getFeedTitle(); - String contentTitle = info.playable.getEpisodeTitle(); - Notification notification = null; - if (android.os.Build.VERSION.SDK_INT >= 16) { - Intent pauseButtonIntent = new Intent( - PlaybackService.this, PlaybackService.class); - pauseButtonIntent.putExtra( - MediaButtonReceiver.EXTRA_KEYCODE, - KeyEvent.KEYCODE_MEDIA_PAUSE); - PendingIntent pauseButtonPendingIntent = PendingIntent - .getService(PlaybackService.this, 0, - pauseButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - Notification.Builder notificationBuilder = new Notification.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setOngoing(true) - .setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna) - .addAction(android.R.drawable.ic_media_pause, - getString(R.string.pause_label), - pauseButtonPendingIntent); - notification = notificationBuilder.build(); - } else { - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna); - notification = notificationBuilder.getNotification(); - } - startForeground(NOTIFICATION_ID, notification); - if (BuildConfig.DEBUG) - Log.d(TAG, "Notification set up"); - } - } - - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - notificationSetupTask - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - notificationSetupTask.execute(); - } - - } - - /** - * Saves the current position of the media file to the DB - * - * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects - * @param deltaPlayedDuration value by which played_duration should be increased. - */ - private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { - int position = getCurrentPosition(); - int duration = getDuration(); - float playbackSpeed = getCurrentPlaybackSpeed(); - final Playable playable = mediaPlayer.getPSMPInfo().playable; - if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Saving current position to " + position); - if (updatePlayedDuration && playable instanceof FeedMedia) { - FeedMedia m = (FeedMedia) playable; - FeedItem item = m.getItem(); - m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); - // Auto flattr - if (isAutoFlattrable(m) && - (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - - if (BuildConfig.DEBUG) - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) - + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); - DBTasks.flattrItemIfLoggedIn(this, item); - } - } - playable.saveCurrentPosition(PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()), - position - ); - } - } - - private void stopWidgetUpdater() { - taskManager.cancelWidgetUpdater(); - sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE)); - } - - private void updateWidget() { - PlaybackService.this.sendBroadcast(new Intent( - PlayerWidget.FORCE_WIDGET_UPDATE)); - } - - public boolean sleepTimerActive() { - return taskManager.isSleepTimerActive(); - } - - public long getSleepTimerTimeLeft() { - return taskManager.getSleepTimerTimeLeft(); - } - - @SuppressLint("NewApi") - private RemoteControlClient setupRemoteControlClient() { - if (Build.VERSION.SDK_INT < 14) { - return null; - } - - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(new ComponentName(getPackageName(), - MediaButtonReceiver.class.getName())); - PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( - getApplicationContext(), 0, mediaButtonIntent, 0); - remoteControlClient = new RemoteControlClient(mediaPendingIntent); - int controlFlags; - if (android.os.Build.VERSION.SDK_INT < 16) { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE - | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; - } else { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; - } - remoteControlClient.setTransportControlFlags(controlFlags); - return remoteControlClient; - } - - /** - * Refresh player status and metadata. - */ - @SuppressLint("NewApi") - private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) { - if (android.os.Build.VERSION.SDK_INT >= 14) { - if (remoteControlClient != null) { - switch (info.playerStatus) { - case PLAYING: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - break; - case PAUSED: - case INITIALIZED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - break; - case STOPPED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - break; - case ERROR: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); - break; - default: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); - } - if (info.playable != null) { - MetadataEditor editor = remoteControlClient - .editMetadata(false); - editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, - info.playable.getEpisodeTitle()); - - editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, - info.playable.getFeedTitle()); - - editor.apply(); - } - if (BuildConfig.DEBUG) - Log.d(TAG, "RemoteControlClient state was refreshed"); - } - } - } - - private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { - boolean isPlaying = false; - - if (info.playerStatus == PlayerStatus.PLAYING) { - isPlaying = true; - } - - if (info.playable != null) { - Intent i = new Intent(whatChanged); - i.putExtra("id", 1); - i.putExtra("artist", ""); - i.putExtra("album", info.playable.getFeedTitle()); - i.putExtra("track", info.playable.getEpisodeTitle()); - i.putExtra("playing", isPlaying); - final List queue = taskManager.getQueueIfLoaded(); - if (queue != null) { - i.putExtra("ListSize", queue.size()); - } - i.putExtra("duration", info.playable.getDuration()); - i.putExtra("position", info.playable.getPosition()); - sendBroadcast(i); - } - } - - /** - * Pauses playback when the headset is disconnected and the preference is - * set - */ - private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { - private static final String TAG = "headsetDisconnected"; - private static final int UNPLUGGED = 0; - - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { - int state = intent.getIntExtra("state", -1); - if (state != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset plug event. State is " + state); - if (state == UNPLUGGED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset was unplugged during playback."); - pauseIfPauseOnDisconnect(); - } - } else { - Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); - } - } - } - }; - - private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - // sound is about to change, eg. bluetooth -> speaker - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback because audio is becoming noisy"); - pauseIfPauseOnDisconnect(); - } - // android.media.AUDIO_BECOMING_NOISY - }; - - /** - * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. - */ - private void pauseIfPauseOnDisconnect() { - if (UserPreferences.isPauseOnHeadsetDisconnect()) { - mediaPlayer.pause(true, true); - } - } - - private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - stopSelf(); - } - } - - }; - - private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - mediaPlayer.endPlayback(); - } - } - }; - - public static MediaType getCurrentMediaType() { - return currentMediaType; - } - - public void resume() { - mediaPlayer.resume(); - } - - public void prepare() { - mediaPlayer.prepare(); - } - - public void pause(boolean abandonAudioFocus, boolean reinit) { - mediaPlayer.pause(abandonAudioFocus, reinit); - } - - public void reinit() { - mediaPlayer.reinit(); - } - - public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { - return mediaPlayer.getPSMPInfo(); - } - - public PlayerStatus getStatus() { - return mediaPlayer.getPSMPInfo().playerStatus; - } - - public Playable getPlayable() { - return mediaPlayer.getPSMPInfo().playable; - } - - public void setSpeed(float speed) { - mediaPlayer.setSpeed(speed); - } - - public boolean canSetSpeed() { - return mediaPlayer.canSetSpeed(); - } - - public float getCurrentPlaybackSpeed() { - return mediaPlayer.getPlaybackSpeed(); - } - - public boolean isStartWhenPrepared() { - return mediaPlayer.isStartWhenPrepared(); - } - - public void setStartWhenPrepared(boolean s) { - mediaPlayer.setStartWhenPrepared(s); - } - - - public void seekTo(final int t) { - mediaPlayer.seekTo(t); - } - - - public void seekDelta(final int d) { - mediaPlayer.seekDelta(d); - } - - /** - * @see de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.feed.Chapter) - */ - public void seekToChapter(Chapter c) { - mediaPlayer.seekToChapter(c); - } - - /** - * call getDuration() on mediaplayer or return INVALID_TIME if player is in - * an invalid state. - */ - public int getDuration() { - return mediaPlayer.getDuration(); - } - - /** - * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player - * is in an invalid state. - */ - public int getCurrentPosition() { - return mediaPlayer.getPosition(); - } - - public boolean isStreaming() { - return mediaPlayer.isStreaming(); - } - - public Pair getVideoSize() { - return mediaPlayer.getVideoSize(); - } - - private boolean isAutoFlattrable(Playable p) { - if (p != null && p instanceof FeedMedia) { - FeedMedia media = (FeedMedia) p; - FeedItem item = ((FeedMedia) p).getItem(); - return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); - } else { - return false; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java deleted file mode 100644 index 49f20012d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java +++ /dev/null @@ -1,979 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -import android.content.ComponentName; -import android.content.Context; -import android.media.AudioManager; -import android.media.RemoteControlClient; -import android.net.wifi.WifiManager; -import android.os.PowerManager; -import android.telephony.TelephonyManager; -import android.util.Log; -import android.util.Pair; -import android.view.SurfaceHolder; - -import org.apache.commons.lang3.Validate; - -import java.io.IOException; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.util.playback.AudioPlayer; -import de.danoeh.antennapod.util.playback.IPlayer; -import de.danoeh.antennapod.util.playback.Playable; -import de.danoeh.antennapod.util.playback.VideoPlayer; - -/** - * Manages the MediaPlayer object of the PlaybackService. - */ -public class PlaybackServiceMediaPlayer { - public static final String TAG = "PlaybackServiceMediaPlayer"; - - /** - * Return value of some PSMP methods if the method call failed. - */ - public static final int INVALID_TIME = -1; - - private final AudioManager audioManager; - - private volatile PlayerStatus playerStatus; - private volatile PlayerStatus statusBeforeSeeking; - private volatile IPlayer mediaPlayer; - private volatile Playable media; - - private volatile boolean stream; - private volatile MediaType mediaType; - private volatile AtomicBoolean startWhenPrepared; - private volatile boolean pausedBecauseOfTransientAudiofocusLoss; - private volatile Pair videoSize; - - /** - * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads - * have to wait until these operations have finished. - */ - private final ReentrantLock playerLock; - - private final PSMPCallback callback; - private final Context context; - - private final ThreadPoolExecutor executor; - - /** - * A wifi-lock that is acquired if the media file is being streamed. - */ - private WifiManager.WifiLock wifiLock; - - public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { - Validate.notNull(context); - Validate.notNull(callback); - - this.context = context; - this.callback = callback; - this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - this.playerLock = new ReentrantLock(); - this.startWhenPrepared = new AtomicBoolean(false); - executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque(), - new RejectedExecutionHandler() { - @Override - public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); - } - } - ); - - mediaPlayer = null; - statusBeforeSeeking = null; - pausedBecauseOfTransientAudiofocusLoss = false; - mediaType = MediaType.UNKNOWN; - playerStatus = PlayerStatus.STOPPED; - videoSize = null; - } - - /** - * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing - * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will - * not do anything. - * Whether playback starts immediately depends on the given parameters. See below for more details. - *

- * States: - * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. - *

- * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If - * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. - *

- * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object - * will enter the ERROR state. - *

- * This method is executed on an internal executor service. - * - * @param playable The Playable object that is supposed to be played. This parameter must not be null. - * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via - * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by - * the Android MediaPlayer via getStreamUrl. - * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the - * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared - * for playback immediately (see 'prepareImmediately' parameter for more details) - * @param prepareImmediately Set to true if the method should also prepare the episode for playback. - */ - public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - Validate.notNull(playable); - - if (BuildConfig.DEBUG) Log.d(TAG, "Play media object."); - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - try { - playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); - } catch (RuntimeException e) { - e.printStackTrace(); - throw e; - } finally { - playerLock.unlock(); - } - } - }); - } - - /** - * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if - * the given playable parameter is the same object as the currently playing media. - *

- * This method requires the playerLock and is executed on the caller's thread. - * - * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean) - */ - private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - Validate.notNull(playable); - if (!playerLock.isHeldByCurrentThread()) - throw new IllegalStateException("method requires playerLock"); - - - if (media != null) { - if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { - // episode is already playing -> ignore method call - if (BuildConfig.DEBUG) - Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); - return; - } else { - // stop playback of this episode - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { - mediaPlayer.stop(); - } - setPlayerStatus(PlayerStatus.INDETERMINATE, null); - } - } - - this.media = playable; - this.stream = stream; - this.mediaType = media.getMediaType(); - this.videoSize = null; - createMediaPlayer(); - PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared); - setPlayerStatus(PlayerStatus.INITIALIZING, media); - try { - media.loadMetadata(); - if (stream) { - mediaPlayer.setDataSource(media.getStreamUrl()); - } else { - mediaPlayer.setDataSource(media.getLocalMediaUrl()); - } - setPlayerStatus(PlayerStatus.INITIALIZED, media); - - if (mediaType == MediaType.VIDEO) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT); - } - - if (prepareImmediately) { - setPlayerStatus(PlayerStatus.PREPARING, media); - mediaPlayer.prepare(); - onPrepared(startWhenPrepared); - } - - } catch (Playable.PlayableException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } catch (IOException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } catch (IllegalStateException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } - } - - - /** - * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. - * nothing will happen. - *

- * This method is executed on an internal executor service. - */ - public void resume() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - resumeSync(); - playerLock.unlock(); - } - }); - } - - private void resumeSync() { - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - int focusGained = audioManager.requestAudioFocus( - audioFocusChangeListener, AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - acquireWifiLockIfNecessary(); - setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); - mediaPlayer.start(); - if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { - mediaPlayer.seekTo(media.getPosition()); - } - - setPlayerStatus(PlayerStatus.PLAYING, media); - pausedBecauseOfTransientAudiofocusLoss = false; - if (android.os.Build.VERSION.SDK_INT >= 14) { - RemoteControlClient remoteControlClient = callback.getRemoteControlClient(); - if (remoteControlClient != null) { - audioManager - .registerRemoteControlClient(remoteControlClient); - } - } - audioManager - .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), - MediaButtonReceiver.class.getName())); - media.onPlaybackStart(); - - } else { - if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); - } - } - - - /** - * Saves the current position and pauses playback. Note that, if audiofocus - * is abandoned, the lockscreen controls will also disapear. - *

- * This method is executed on an internal executor service. - * - * @param abandonFocus is true if the service should release audio focus - * @param reinit is true if service should reinit after pausing if the media - * file is being streamed - */ - public void pause(final boolean abandonFocus, final boolean reinit) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback."); - mediaPlayer.pause(); - setPlayerStatus(PlayerStatus.PAUSED, media); - - if (abandonFocus) { - audioManager.abandonAudioFocus(audioFocusChangeListener); - pausedBecauseOfTransientAudiofocusLoss = false; - } - if (stream && reinit) { - reinit(); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); - } - - playerLock.unlock(); - } - }); - } - - /** - * Prepared media player for playback if the service is in the INITALIZED - * state. - *

- * This method is executed on an internal executor service. - */ - public void prepare() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - - if (playerStatus == PlayerStatus.INITIALIZED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Preparing media player"); - setPlayerStatus(PlayerStatus.PREPARING, media); - try { - mediaPlayer.prepare(); - onPrepared(startWhenPrepared.get()); - } catch (IOException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } - } - playerLock.unlock(); - - } - }); - } - - /** - * Called after media player has been prepared. This method is executed on the caller's thread. - */ - void onPrepared(final boolean startWhenPrepared) { - playerLock.lock(); - - if (playerStatus != PlayerStatus.PREPARING) { - playerLock.unlock(); - throw new IllegalStateException("Player is not in PREPARING state"); - } - - if (BuildConfig.DEBUG) - Log.d(TAG, "Resource prepared"); - - if (mediaType == MediaType.VIDEO) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight()); - } - - if (media.getPosition() > 0) { - mediaPlayer.seekTo(media.getPosition()); - } - - if (media.getDuration() == 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting duration of media"); - media.setDuration(mediaPlayer.getDuration()); - } - setPlayerStatus(PlayerStatus.PREPARED, media); - - if (startWhenPrepared) { - resumeSync(); - } - - playerLock.unlock(); - } - - /** - * Resets the media player and moves it into INITIALIZED state. - *

- * This method is executed on an internal executor service. - */ - public void reinit() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - if (media != null) { - playMediaObject(media, true, stream, startWhenPrepared.get(), false); - } else if (mediaPlayer != null) { - mediaPlayer.reset(); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); - } - playerLock.unlock(); - } - }); - } - - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * - * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0 - *

- * This method is executed on the caller's thread. - */ - private void seekToSync(int t) { - if (t < 0) { - t = 0; - } - playerLock.lock(); - - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - if (stream) { - // statusBeforeSeeking = playerStatus; - // setPlayerStatus(PlayerStatus.SEEKING, media); - } - mediaPlayer.seekTo(t); - - } else if (playerStatus == PlayerStatus.INITIALIZED) { - media.setPosition(t); - startWhenPrepared.set(true); - prepare(); - } - playerLock.unlock(); - } - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * Invalid time values (< 0) will be ignored. - *

- * This method is executed on an internal executor service. - */ - public void seekTo(final int t) { - executor.submit(new Runnable() { - @Override - public void run() { - seekToSync(t); - } - }); - } - - /** - * Seek a specific position from the current position - * - * @param d offset from current position (positive or negative) - */ - public void seekDelta(final int d) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - int currentPosition = getPosition(); - if (currentPosition != INVALID_TIME) { - seekToSync(currentPosition + d); - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); - } - - playerLock.unlock(); - } - }); - } - - /** - * Seek to the start of the specified chapter. - */ - public void seekToChapter(Chapter c) { - Validate.notNull(c); - - seekTo((int) c.getStart()); - } - - /** - * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. - */ - public int getDuration() { - if (!playerLock.tryLock()) { - return INVALID_TIME; - } - - int retVal = INVALID_TIME; - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - retVal = mediaPlayer.getDuration(); - } else if (media != null && media.getDuration() > 0) { - retVal = media.getDuration(); - } - - playerLock.unlock(); - return retVal; - } - - /** - * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. - */ - public int getPosition() { - if (!playerLock.tryLock()) { - return INVALID_TIME; - } - - int retVal = INVALID_TIME; - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - retVal = mediaPlayer.getCurrentPosition(); - } else if (media != null && media.getPosition() > 0) { - retVal = media.getPosition(); - } - - playerLock.unlock(); - return retVal; - } - - public boolean isStartWhenPrepared() { - return startWhenPrepared.get(); - } - - public void setStartWhenPrepared(boolean startWhenPrepared) { - this.startWhenPrepared.set(startWhenPrepared); - } - - /** - * Returns true if the playback speed can be adjusted. - */ - public boolean canSetSpeed() { - boolean retVal = false; - if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { - retVal = (mediaPlayer).canSetSpeed(); - } - return retVal; - } - - /** - * Sets the playback speed. - * This method is executed on the caller's thread. - */ - private void setSpeedSync(float speed) { - playerLock.lock(); - if (media != null && media.getMediaType() == MediaType.AUDIO) { - if (mediaPlayer.canSetSpeed()) { - mediaPlayer.setPlaybackSpeed((float) speed); - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback speed was set to " + speed); - callback.playbackSpeedChanged(speed); - } - } - playerLock.unlock(); - } - - /** - * Sets the playback speed. - * This method is executed on an internal executor service. - */ - public void setSpeed(final float speed) { - executor.submit(new Runnable() { - @Override - public void run() { - setSpeedSync(speed); - } - }); - } - - /** - * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. - */ - public float getPlaybackSpeed() { - if (!playerLock.tryLock()) { - return 1; - } - - float retVal = 1; - if ((playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) { - retVal = mediaPlayer.getCurrentSpeedMultiplier(); - } - playerLock.unlock(); - return retVal; - } - - public MediaType getCurrentMediaType() { - return mediaType; - } - - public boolean isStreaming() { - return stream; - } - - - /** - * Releases internally used resources. This method should only be called when the object is not used anymore. - */ - public void shutdown() { - executor.shutdown(); - if (mediaPlayer != null) { - mediaPlayer.release(); - } - releaseWifiLockIfNecessary(); - } - - public void setVideoSurface(final SurfaceHolder surface) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - if (mediaPlayer != null) { - mediaPlayer.setDisplay(surface); - } - playerLock.unlock(); - } - }); - } - - public void resetVideoSurface() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting video surface"); - mediaPlayer.setDisplay(null); - reinit(); - playerLock.unlock(); - } - }); - } - - /** - * Return width and height of the currently playing video as a pair. - * - * @return Width and height as a Pair or null if the video size could not be determined. The method might still - * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return - * invalid values. - */ - public Pair getVideoSize() { - if (!playerLock.tryLock()) { - // use cached value if lock can't be aquired - return videoSize; - } - Pair res; - if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { - res = null; - } else { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair(vp.getVideoWidth(), vp.getVideoHeight()); - res = videoSize; - } - playerLock.unlock(); - return res; - } - - /** - * Returns a PSMInfo object that contains information about the current state of the PSMP object. - * - * @return The PSMPInfo object. - */ - public synchronized PSMPInfo getPSMPInfo() { - return new PSMPInfo(playerStatus, media); - } - - /** - * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time - * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). - *

- * This method will notify the callback about the change of the player status (even if the new status is the same - * as the old one). - * - * @param newStatus The new PlayerStatus. This must not be null. - * @param newMedia The new playable object of the PSMP object. This can be null. - */ - private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { - Validate.notNull(newStatus); - - if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); - - this.playerStatus = newStatus; - this.media = newMedia; - callback.statusChanged(new PSMPInfo(playerStatus, media)); - } - - private IPlayer createMediaPlayer() { - if (mediaPlayer != null) { - mediaPlayer.release(); - } - if (media == null || media.getMediaType() == MediaType.VIDEO) { - mediaPlayer = new VideoPlayer(); - } else { - mediaPlayer = new AudioPlayer(context); - } - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); - return setMediaPlayerListeners(mediaPlayer); - } - - private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { - - @Override - public void onAudioFocusChange(final int focusChange) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - - // If there is an incoming call, playback should be paused permanently - TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - final int callState = (tm != null) ? tm.getCallState() : 0; - if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState); - Log.i(TAG, "Call state:" + callState); - - if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); - pause(true, false); - callback.shouldStop(); - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); - if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now - resume(); - } else { // we ducked => raise audio level back - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, 0); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - if (playerStatus == PlayerStatus.PLAYING) { - if (!UserPreferences.shouldPauseForFocusLoss()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, 0); - pausedBecauseOfTransientAudiofocusLoss = false; - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { - if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - playerLock.unlock(); - } - } - }); - } - }; - - - public void endPlayback() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - if (playerStatus != PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - } - if (mediaPlayer != null) { - mediaPlayer.reset(); - - } - audioManager.abandonAudioFocus(audioFocusChangeListener); - callback.endPlayback(true); - - playerLock.unlock(); - } - }); - } - - /** - * Moves the PlaybackServiceMediaPlayer into STOPPED state. This call is only valid if the player is currently in - * INDETERMINATE state, for example after a call to endPlayback. - * This method will only take care of changing the PlayerStatus of this object! Other tasks like - * abandoning audio focus have to be done with other methods. - */ - public void stop() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - if (playerStatus == PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.STOPPED, null); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); - } - playerLock.unlock(); - - } - }); - } - - private synchronized void acquireWifiLockIfNecessary() { - if (stream) { - if (wifiLock == null) { - wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) - .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); - wifiLock.setReferenceCounted(false); - } - wifiLock.acquire(); - } - } - - private synchronized void releaseWifiLockIfNecessary() { - if (wifiLock != null && wifiLock.isHeld()) { - wifiLock.release(); - } - } - - /** - * Holds information about a PSMP object. - */ - public class PSMPInfo { - public PlayerStatus playerStatus; - public Playable playable; - - public PSMPInfo(PlayerStatus playerStatus, Playable playable) { - this.playerStatus = playerStatus; - this.playable = playable; - } - } - - public static interface PSMPCallback { - public void statusChanged(PSMPInfo newInfo); - - public void shouldStop(); - - public void playbackSpeedChanged(float s); - - public void onBufferingUpdate(int percent); - - public boolean onMediaPlayerInfo(int code); - - public boolean onMediaPlayerError(Object inObj, int what, int extra); - - public boolean endPlayback(boolean playNextEpisode); - - public RemoteControlClient getRemoteControlClient(); - } - - private IPlayer setMediaPlayerListeners(IPlayer mp) { - if (mp != null && media != null) { - if (media.getMediaType() == MediaType.AUDIO) { - ((AudioPlayer) mp) - .setOnCompletionListener(audioCompletionListener); - ((AudioPlayer) mp) - .setOnSeekCompleteListener(audioSeekCompleteListener); - ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); - ((AudioPlayer) mp) - .setOnBufferingUpdateListener(audioBufferingUpdateListener); - ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); - } else { - ((VideoPlayer) mp) - .setOnCompletionListener(videoCompletionListener); - ((VideoPlayer) mp) - .setOnSeekCompleteListener(videoSeekCompleteListener); - ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); - ((VideoPlayer) mp) - .setOnBufferingUpdateListener(videoBufferingUpdateListener); - ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); - } - } - return mp; - } - - private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(com.aocate.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(android.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private void genericOnCompletion() { - endPlayback(); - } - - private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, - int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private void genericOnBufferingUpdate(int percent) { - callback.onBufferingUpdate(percent); - } - - private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericInfoListener(what); - } - }; - - private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { - return genericInfoListener(what); - } - }; - - private boolean genericInfoListener(int what) { - return callback.onMediaPlayerInfo(what); - } - - private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericOnError(mp, what, extra); - } - }; - - private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(android.media.MediaPlayer mp, int what, int extra) { - return genericOnError(mp, what, extra); - } - }; - - private boolean genericOnError(Object inObj, int what, int extra) { - return callback.onMediaPlayerError(inObj, what, extra); - } - - private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(com.aocate.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(android.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final void genericSeekCompleteListener() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media); - } - playerLock.unlock(); - } - }); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java deleted file mode 100644 index 680ec2e2f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java +++ /dev/null @@ -1,384 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -import android.content.Context; -import android.util.Log; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.util.playback.Playable; - -import java.util.List; -import java.util.concurrent.*; - -/** - * Manages the background tasks of PlaybackSerivce, i.e. - * the sleep timer, the position saver, the widget updater and - * the queue loader. - *

- * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) - * to notify the PlaybackService about updates from the running tasks. - */ -public class PlaybackServiceTaskManager { - private static final String TAG = "PlaybackServiceTaskManager"; - - /** - * Update interval of position saver in milliseconds. - */ - public static final int POSITION_SAVER_WAITING_INTERVAL = 5000; - /** - * Notification interval of widget updater in milliseconds. - */ - public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500; - - private static final int SCHED_EX_POOL_SIZE = 2; - private final ScheduledThreadPoolExecutor schedExecutor; - - private ScheduledFuture positionSaverFuture; - private ScheduledFuture widgetUpdaterFuture; - private ScheduledFuture sleepTimerFuture; - private volatile Future> queueFuture; - private volatile Future chapterLoaderFuture; - - private SleepTimer sleepTimer; - - private final Context context; - private final PSTMCallback callback; - - /** - * Sets up a new PSTM. This method will also start the queue loader task. - * - * @param context - * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. - */ - public PlaybackServiceTaskManager(Context context, PSTMCallback callback) { - Validate.notNull(context); - Validate.notNull(callback); - - this.context = context; - this.callback = callback; - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - loadQueue(); - EventDistributor.getInstance().register(eventDistributorListener); - } - - private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { - cancelQueueLoader(); - loadQueue(); - } - } - }; - - private synchronized boolean isQueueLoaderActive() { - return queueFuture != null && !queueFuture.isDone(); - } - - private synchronized void cancelQueueLoader() { - if (isQueueLoaderActive()) { - queueFuture.cancel(true); - } - } - - private synchronized void loadQueue() { - if (!isQueueLoaderActive()) { - queueFuture = schedExecutor.submit(new Callable>() { - @Override - public List call() throws Exception { - return DBReader.getQueue(context); - } - }); - } - } - - /** - * Returns the queue if it is already loaded or null if it hasn't been loaded yet. - * In order to wait until the queue has been loaded, use getQueue() - */ - public synchronized List getQueueIfLoaded() { - if (queueFuture.isDone()) { - try { - return queueFuture.get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - return null; - } - - /** - * Returns the queue or waits until the PSTM has loaded the queue from the database. - */ - public synchronized List getQueue() throws InterruptedException { - try { - return queueFuture.get(); - } catch (ExecutionException e) { - throw new IllegalArgumentException(e); - } - } - - /** - * Starts the position saver task. If the position saver is already active, nothing will happen. - */ - public synchronized void startPositionSaver() { - if (!isPositionSaverActive()) { - Runnable positionSaver = new Runnable() { - @Override - public void run() { - callback.positionSaverTick(); - } - }; - positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, - POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); - - if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); - } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); - } - } - - /** - * Returns true if the position saver is currently running. - */ - public synchronized boolean isPositionSaverActive() { - return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone(); - } - - /** - * Cancels the position saver. If the position saver is not running, nothing will happen. - */ - public synchronized void cancelPositionSaver() { - if (isPositionSaverActive()) { - positionSaverFuture.cancel(false); - if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver"); - } - } - - /** - * Starts the widget updater task. If the widget updater is already active, nothing will happen. - */ - public synchronized void startWidgetUpdater() { - if (!isWidgetUpdaterActive()) { - Runnable widgetUpdater = new Runnable() { - @Override - public void run() { - callback.onWidgetUpdaterTick(); - } - }; - widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, - WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); - - if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater"); - } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored."); - } - } - - /** - * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be - * cancelled first. - * After waitingTime has elapsed, onSleepTimerExpired() will be called. - * - * @throws java.lang.IllegalArgumentException if waitingTime <= 0 - */ - public synchronized void setSleepTimer(long waitingTime) { - Validate.isTrue(waitingTime > 0, "Waiting time <= 0"); - - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); - if (isSleepTimerActive()) { - sleepTimerFuture.cancel(true); - } - sleepTimer = new SleepTimer(waitingTime); - sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); - } - - /** - * Returns true if the sleep timer is currently active. - */ - public synchronized boolean isSleepTimerActive() { - return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting; - } - - /** - * Disables the sleep timer. If the sleep timer is not active, nothing will happen. - */ - public synchronized void disableSleepTimer() { - if (isSleepTimerActive()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Disabling sleep timer"); - sleepTimerFuture.cancel(true); - } - } - - /** - * Returns the current sleep timer time or 0 if the sleep timer is not active. - */ - public synchronized long getSleepTimerTimeLeft() { - if (isSleepTimerActive()) { - return sleepTimer.getWaitingTime(); - } else { - return 0; - } - } - - - /** - * Returns true if the widget updater is currently running. - */ - public synchronized boolean isWidgetUpdaterActive() { - return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone(); - } - - /** - * Cancels the widget updater. If the widget updater is not running, nothing will happen. - */ - public synchronized void cancelWidgetUpdater() { - if (isWidgetUpdaterActive()) { - widgetUpdaterFuture.cancel(false); - if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater"); - } - } - - private synchronized void cancelChapterLoader() { - if (isChapterLoaderActive()) { - chapterLoaderFuture.cancel(true); - } - } - - private synchronized boolean isChapterLoaderActive() { - return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); - } - - /** - * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, - * it will be cancelled first. - * On completion, the callback's onChapterLoaded method will be called. - */ - public synchronized void startChapterLoader(final Playable media) { - Validate.notNull(media); - - if (isChapterLoaderActive()) { - cancelChapterLoader(); - } - - Runnable chapterLoader = new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Chapter loader started"); - if (media.getChapters() == null) { - media.loadChapterMarks(); - if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) { - callback.onChapterLoaded(media); - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Chapter loader stopped"); - } - }; - chapterLoaderFuture = schedExecutor.submit(chapterLoader); - } - - - /** - * Cancels all tasks. The PSTM will be in the initial state after execution of this method. - */ - public synchronized void cancelAllTasks() { - cancelPositionSaver(); - cancelWidgetUpdater(); - disableSleepTimer(); - cancelQueueLoader(); - cancelChapterLoader(); - } - - /** - * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after - * execution of this method. - */ - public synchronized void shutdown() { - EventDistributor.getInstance().unregister(eventDistributorListener); - cancelAllTasks(); - schedExecutor.shutdown(); - } - - /** - * Sleeps for a given time and then pauses playback. - */ - private class SleepTimer implements Runnable { - private static final String TAG = "SleepTimer"; - private static final long UPDATE_INTERVALL = 1000L; - private volatile long waitingTime; - private volatile boolean isWaiting; - - public SleepTimer(long waitingTime) { - super(); - this.waitingTime = waitingTime; - isWaiting = true; - } - - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting"); - while (waitingTime > 0) { - try { - Thread.sleep(UPDATE_INTERVALL); - waitingTime -= UPDATE_INTERVALL; - - if (waitingTime <= 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting completed"); - postExecute(); - if (!Thread.currentThread().isInterrupted()) { - callback.onSleepTimerExpired(); - } - - } - } catch (InterruptedException e) { - Log.d(TAG, "Thread was interrupted while waiting"); - break; - } - } - postExecute(); - } - - protected void postExecute() { - isWaiting = false; - } - - public long getWaitingTime() { - return waitingTime; - } - - public boolean isWaiting() { - return isWaiting; - } - - } - - public static interface PSTMCallback { - void positionSaverTick(); - - void onSleepTimerExpired(); - - void onWidgetUpdaterTick(); - - void onChapterLoaded(Playable media); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/service/playback/PlayerStatus.java b/app/src/main/java/de/danoeh/antennapod/service/playback/PlayerStatus.java deleted file mode 100644 index 3d2b4ad39..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/playback/PlayerStatus.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -public enum PlayerStatus { - INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state. - ERROR, - PREPARING, - PAUSED, - PLAYING, - STOPPED, - PREPARED, - SEEKING, - INITIALIZING, // playback service is loading the Playable's metadata - INITIALIZED // playback service was started, data source of media player was set. -} diff --git a/app/src/main/java/de/danoeh/antennapod/service/playback/PlayerWidgetService.java b/app/src/main/java/de/danoeh/antennapod/service/playback/PlayerWidgetService.java deleted file mode 100644 index 71bc40c2a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/service/playback/PlayerWidgetService.java +++ /dev/null @@ -1,190 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -import android.app.PendingIntent; -import android.app.Service; -import android.appwidget.AppWidgetManager; -import android.content.ComponentName; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Build; -import android.os.IBinder; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; -import android.widget.RemoteViews; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.receiver.PlayerWidget; -import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.playback.Playable; - -/** Updates the state of the player widget */ -public class PlayerWidgetService extends Service { - private static final String TAG = "PlayerWidgetService"; - - private PlaybackService playbackService; - /** True while service is updating the widget */ - private volatile boolean isUpdating; - - public PlayerWidgetService() { - } - - @Override - public void onCreate() { - super.onCreate(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service created"); - isUpdating = false; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); - try { - unbindService(mConnection); - } catch (IllegalArgumentException e) { - Log.w(TAG, "IllegalArgumentException when trying to unbind service"); - } - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (!isUpdating) { - if (playbackService == null && PlaybackService.isRunning) { - bindService(new Intent(this, PlaybackService.class), - mConnection, 0); - } else { - startViewUpdaterIfNotRunning(); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Service was called while updating. Ignoring update request"); - } - return Service.START_NOT_STICKY; - } - - private void updateViews() { - if (playbackService == null) { - return; - } - isUpdating = true; - - ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); - AppWidgetManager manager = AppWidgetManager.getInstance(this); - RemoteViews views = new RemoteViews(getPackageName(), - R.layout.player_widget); - PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0, - PlaybackService.getPlayerActivityIntent(this), 0); - - views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer); - final Playable media = playbackService.getPlayable(); - if (playbackService != null && media != null) { - PlayerStatus status = playbackService.getStatus(); - - views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); - - if (status == PlayerStatus.PLAYING) { - String progressString = getProgressString(playbackService); - if (progressString != null) { - views.setTextViewText(R.id.txtvProgress, progressString); - } - views.setImageViewResource(R.id.butPlay, R.drawable.av_pause_dark); - if (Build.VERSION.SDK_INT >= 15) { - views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); - } - } else { - views.setImageViewResource(R.id.butPlay, R.drawable.av_play_dark); - if (Build.VERSION.SDK_INT >= 15) { - views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); - } - } - views.setOnClickPendingIntent(R.id.butPlay, - createMediaButtonIntent()); - } else { - views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE); - views.setTextViewText(R.id.txtvTitle, - this.getString(R.string.no_media_playing_label)); - views.setImageViewResource(R.id.butPlay, R.drawable.av_play); - - } - - manager.updateAppWidget(playerWidget, views); - isUpdating = false; - } - - /** Creates an intent which fakes a mediabutton press */ - private PendingIntent createMediaButtonIntent() { - KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); - Intent startingIntent = new Intent( - MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); - startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - - return PendingIntent.getBroadcast(this, 0, startingIntent, 0); - } - - private String getProgressString(PlaybackService ps) { - int position = ps.getCurrentPosition(); - int duration = ps.getDuration(); - if (position != PlaybackService.INVALID_TIME - && duration != PlaybackService.INVALID_TIME) { - return Converter.getDurationStringLong(position) + " / " - + Converter.getDurationStringLong(duration); - } else { - return null; - } - } - - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Connection to service established"); - playbackService = ((PlaybackService.LocalBinder) service) - .getService(); - startViewUpdaterIfNotRunning(); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - playbackService = null; - if (BuildConfig.DEBUG) - Log.d(TAG, "Disconnected from service"); - } - - }; - - private void startViewUpdaterIfNotRunning() { - if (!isUpdating) { - ViewUpdater updateThread = new ViewUpdater(this); - updateThread.start(); - } - } - - static class ViewUpdater extends Thread { - private static final String THREAD_NAME = "ViewUpdater"; - private PlayerWidgetService service; - - public ViewUpdater(PlayerWidgetService service) { - super(); - setName(THREAD_NAME); - this.service = service; - - } - - @Override - public void run() { - service.updateViews(); - } - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/DBReader.java b/app/src/main/java/de/danoeh/antennapod/storage/DBReader.java deleted file mode 100644 index e49ea4f83..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/DBReader.java +++ /dev/null @@ -1,908 +0,0 @@ -package de.danoeh.antennapod.storage; - -import android.content.Context; -import android.database.Cursor; -import android.database.SQLException; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.service.download.DownloadStatus; -import de.danoeh.antennapod.util.DownloadError; -import de.danoeh.antennapod.util.comparator.DownloadStatusComparator; -import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.util.comparator.PlaybackCompletionDateComparator; -import de.danoeh.antennapod.util.flattr.FlattrStatus; -import de.danoeh.antennapod.util.flattr.FlattrThing; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -/** - * Provides methods for reading data from the AntennaPod database. - * In general, all database calls in DBReader-methods are executed on the caller's thread. - * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread. - * This class will use the {@link de.danoeh.antennapod.feed.EventDistributor} to notify listeners about changes in the database. - */ -public final class DBReader { - private static final String TAG = "DBReader"; - - /** - * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}. - */ - public static final int PLAYBACK_HISTORY_SIZE = 50; - - /** - * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}. - */ - public static final int DOWNLOAD_LOG_SIZE = 200; - - - private DBReader() { - } - - /** - * Returns a list of Feeds, sorted alphabetically by their title. - * - * @param context A context that is used for opening a database connection. - * @return A list of Feeds, sorted alphabetically by their title. A Feed-object - * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list - * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)}. - */ - public static List getFeedList(final Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting Feedlist"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - List result = getFeedList(adapter); - adapter.close(); - return result; - } - - private static List getFeedList(PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting Feedlist"); - - Cursor feedlistCursor = adapter.getAllFeedsCursor(); - List feeds = new ArrayList(feedlistCursor.getCount()); - - if (feedlistCursor.moveToFirst()) { - do { - Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); - feeds.add(feed); - } while (feedlistCursor.moveToNext()); - } - feedlistCursor.close(); - return feeds; - } - - /** - * Returns a list with the download URLs of all feeds. - * - * @param context A context that is used for opening the database connection. - * @return A list of Strings with the download URLs of all feeds. - */ - public static List getFeedListDownloadUrls(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - List result = new ArrayList(); - adapter.open(); - Cursor feeds = adapter.getFeedCursorDownloadUrls(); - if (feeds.moveToFirst()) { - do { - result.add(feeds.getString(1)); - } while (feeds.moveToNext()); - } - feeds.close(); - adapter.close(); - - return result; - } - - /** - * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time. - * - * @param context A context that is used for opening a database connection. - * @param expirationTime Time that is used for determining whether a feed is outdated or not. - * A Feed is considered expired if 'lastUpdate < (currentTime - expirationTime)' evaluates to true. - * @return A list of Feeds, sorted alphabetically by their title. A Feed-object - * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list - * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)}. - */ - public static List getExpiredFeedsList(final Context context, final long expirationTime) { - if (BuildConfig.DEBUG) - Log.d(TAG, String.format("getExpiredFeedsList(%d)", expirationTime)); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - - Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime); - List feeds = new ArrayList(feedlistCursor.getCount()); - - if (feedlistCursor.moveToFirst()) { - do { - Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); - feeds.add(feed); - } while (feedlistCursor.moveToNext()); - } - feedlistCursor.close(); - return feeds; - } - - /** - * Takes a list of FeedItems and loads their corresponding Feed-objects from the database. - * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will - * not find the correct feed of an item. - * - * @param context A context that is used for opening a database connection. - * @param items The FeedItems whose Feed-objects should be loaded. - */ - public static void loadFeedDataOfFeedItemlist(Context context, - List items) { - List feeds = getFeedList(context); - for (FeedItem item : items) { - for (Feed feed : feeds) { - if (feed.getId() == item.getFeedId()) { - item.setFeed(feed); - break; - } - } - if (item.getFeed() == null) { - Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId()); - } - } - } - - /** - * Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not - * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead. - * - * @param context A context that is used for opening a database connection. - * @param feed The Feed whose items should be loaded - * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly. - * The method does NOT change the items-attribute of the feed. - */ - public static List getFeedItemList(Context context, - final Feed feed) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - - Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed); - List items = extractItemlistFromCursor(adapter, - itemlistCursor); - itemlistCursor.close(); - - Collections.sort(items, new FeedItemPubdateComparator()); - - adapter.close(); - - for (FeedItem item : items) { - item.setFeed(feed); - } - - return items; - } - - static List extractItemlistFromCursor(Context context, Cursor itemlistCursor) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - List result = extractItemlistFromCursor(adapter, itemlistCursor); - adapter.close(); - return result; - } - - private static List extractItemlistFromCursor( - PodDBAdapter adapter, Cursor itemlistCursor) { - ArrayList itemIds = new ArrayList(); - List items = new ArrayList( - itemlistCursor.getCount()); - - if (itemlistCursor.moveToFirst()) { - do { - FeedItem item = new FeedItem(); - - item.setId(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID)); - item.setTitle(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_TITLE)); - item.setLink(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_LINK)); - item.setPubDate(new Date(itemlistCursor - .getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE))); - item.setPaymentLink(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK)); - item.setFeedId(itemlistCursor - .getLong(PodDBAdapter.IDX_FI_SMALL_FEED)); - itemIds.add(String.valueOf(item.getId())); - - item.setRead((itemlistCursor - .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0)); - item.setItemIdentifier(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); - item.setFlattrStatus(new FlattrStatus(itemlistCursor - .getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS))); - - long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE); - if (imageIndex != 0) { - item.setImage(getFeedImage(adapter, imageIndex)); - } - - // extract chapters - boolean hasSimpleChapters = itemlistCursor - .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0; - if (hasSimpleChapters) { - Cursor chapterCursor = adapter - .getSimpleChaptersOfFeedItemCursor(item); - if (chapterCursor.moveToFirst()) { - item.setChapters(new ArrayList()); - do { - int chapterType = chapterCursor - .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX); - Chapter chapter = null; - long start = chapterCursor - .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX); - String title = chapterCursor - .getString(PodDBAdapter.KEY_TITLE_INDEX); - String link = chapterCursor - .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX); - - switch (chapterType) { - case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER: - chapter = new SimpleChapter(start, title, item, - link); - break; - case ID3Chapter.CHAPTERTYPE_ID3CHAPTER: - chapter = new ID3Chapter(start, title, item, - link); - break; - case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER: - chapter = new VorbisCommentChapter(start, - title, item, link); - break; - } - if (chapter != null) { - chapter.setId(chapterCursor - .getLong(PodDBAdapter.KEY_ID_INDEX)); - item.getChapters().add(chapter); - } - } while (chapterCursor.moveToNext()); - } - chapterCursor.close(); - } - items.add(item); - } while (itemlistCursor.moveToNext()); - } - - extractMediafromItemlist(adapter, items, itemIds); - return items; - } - - private static void extractMediafromItemlist(PodDBAdapter adapter, - List items, ArrayList itemIds) { - - List itemsCopy = new ArrayList(items); - Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds - .toArray(new String[itemIds.size()])); - if (cursor.moveToFirst()) { - do { - long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); - // find matching feed item - FeedItem item = getMatchingItemForMedia(itemId, itemsCopy); - if (item != null) { - item.setMedia(extractFeedMediaFromCursorRow(cursor)); - item.getMedia().setItem(item); - } - } while (cursor.moveToNext()); - cursor.close(); - } - } - - private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) { - long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); - Date playbackCompletionDate = null; - long playbackCompletionTime = cursor - .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); - if (playbackCompletionTime > 0) { - playbackCompletionDate = new Date( - playbackCompletionTime); - } - - return new FeedMedia( - mediaId, - null, - cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), - cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), - cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), - cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), - cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), - cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), - cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, - playbackCompletionDate, - cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX)); - } - - private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, - Cursor cursor) { - Date lastUpdate = new Date( - cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_LASTUPDATE)); - - final FeedImage image; - long imageIndex = cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_IMAGE); - if (imageIndex != 0) { - image = getFeedImage(adapter, imageIndex); - } else { - image = null; - } - Feed feed = new Feed(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID), - lastUpdate, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TITLE), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LINK), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DESCRIPTION), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_PAYMENT_LINK), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_AUTHOR), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LANGUAGE), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TYPE), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FEED_IDENTIFIER), - image, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0, - new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS))); - - if (image != null) { - image.setOwner(feed); - } - - FeedPreferences preferences = new FeedPreferences(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_USERNAME), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_PASSWORD)); - - feed.setPreferences(preferences); - return feed; - } - - private static FeedItem getMatchingItemForMedia(long itemId, - List items) { - for (FeedItem item : items) { - if (item.getId() == itemId) { - return item; - } - } - return null; - } - - static List getQueue(Context context, PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting queue"); - - Cursor itemlistCursor = adapter.getQueueCursor(); - List items = extractItemlistFromCursor(adapter, - itemlistCursor); - itemlistCursor.close(); - loadFeedDataOfFeedItemlist(context, items); - - return items; - } - - /** - * Loads the IDs of the FeedItems in the queue. This method should be preferred over - * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed. - * - * @param context A context that is used for opening a database connection. - * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned - * list in a {@link de.danoeh.antennapod.util.QueueAccess} object for easier access to the queue's properties. - */ - public static List getQueueIDList(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - - adapter.open(); - List result = getQueueIDList(adapter); - adapter.close(); - - return result; - } - - static List getQueueIDList(PodDBAdapter adapter) { - adapter.open(); - Cursor queueCursor = adapter.getQueueIDCursor(); - - List queueIds = new ArrayList(queueCursor.getCount()); - if (queueCursor.moveToFirst()) { - do { - queueIds.add(queueCursor.getLong(0)); - } while (queueCursor.moveToNext()); - } - return queueIds; - } - - - /** - * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using - * {@link #getQueueIDList(android.content.Context)} instead. - * - * @param context A context that is used for opening a database connection. - * @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned - * list in a {@link de.danoeh.antennapod.util.QueueAccess} object for easier access to the queue's properties. - */ - public static List getQueue(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting queue"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - List items = getQueue(context, adapter); - adapter.close(); - return items; - } - - /** - * Loads a list of FeedItems whose episode has been downloaded. - * - * @param context A context that is used for opening a database connection. - * @return A list of FeedItems whose episdoe has been downloaded. - */ - public static List getDownloadedItems(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting downloaded items"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - - Cursor itemlistCursor = adapter.getDownloadedItemsCursor(); - List items = extractItemlistFromCursor(adapter, - itemlistCursor); - itemlistCursor.close(); - loadFeedDataOfFeedItemlist(context, items); - Collections.sort(items, new FeedItemPubdateComparator()); - - adapter.close(); - return items; - - } - - /** - * Loads a list of FeedItems whose 'read'-attribute is set to false. - * - * @param context A context that is used for opening a database connection. - * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used, - * consider using {@link #getUnreadItemIds(android.content.Context)} instead. - */ - public static List getUnreadItemsList(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting unread items list"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - - Cursor itemlistCursor = adapter.getUnreadItemsCursor(); - List items = extractItemlistFromCursor(adapter, - itemlistCursor); - itemlistCursor.close(); - - loadFeedDataOfFeedItemlist(context, items); - - adapter.close(); - - return items; - } - - /** - * Loads the IDs of the FeedItems whose 'read'-attribute is set to false. - * - * @param context A context that is used for opening a database connection. - * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred - * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used. - */ - public static long[] getUnreadItemIds(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor cursor = adapter.getUnreadItemIdsCursor(); - long[] itemIds = new long[cursor.getCount()]; - int i = 0; - if (cursor.moveToFirst()) { - do { - itemIds[i] = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); - i++; - } while (cursor.moveToNext()); - } - return itemIds; - } - - - /** - * Loads a list of FeedItems sorted by pubDate in descending order. - * - * @param context A context that is used for opening a database connection. - * @param limit The maximum number of episodes that should be loaded. - */ - public static List getRecentlyPublishedEpisodes(Context context, int limit) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting recently published items list"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - - Cursor itemlistCursor = adapter.getRecentlyPublishedItemsCursor(limit); - List items = extractItemlistFromCursor(adapter, - itemlistCursor); - itemlistCursor.close(); - - loadFeedDataOfFeedItemlist(context, items); - - adapter.close(); - - return items; - } - - /** - * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode - * has been completed at least once. - * - * @param context A context that is used for opening a database connection. - * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order. - * The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}. - */ - public static List getPlaybackHistory(final Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading playback history"); - final int PLAYBACK_HISTORY_SIZE = 50; - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - - Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE); - String[] itemIds = new String[mediaCursor.getCount()]; - for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) { - itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX)); - } - mediaCursor.close(); - Cursor itemCursor = adapter.getFeedItemCursor(itemIds); - List items = extractItemlistFromCursor(adapter, itemCursor); - loadFeedDataOfFeedItemlist(context, items); - itemCursor.close(); - adapter.close(); - - Collections.sort(items, new PlaybackCompletionDateComparator()); - return items; - } - - /** - * Loads the download log from the database. - * - * @param context A context that is used for opening a database connection. - * @return A list with DownloadStatus objects that represent the download log. - * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}. - */ - public static List getDownloadLog(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting DownloadLog"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE); - List downloadLog = new ArrayList( - logCursor.getCount()); - - if (logCursor.moveToFirst()) { - do { - long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - - long feedfileId = logCursor - .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); - int feedfileType = logCursor - .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); - boolean successful = logCursor - .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; - int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX); - String reasonDetailed = logCursor - .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); - String title = logCursor - .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); - Date completionDate = new Date( - logCursor - .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX) - ); - downloadLog.add(new DownloadStatus(id, title, feedfileId, - feedfileType, successful, DownloadError.fromCode(reason), completionDate, - reasonDetailed)); - - } while (logCursor.moveToNext()); - } - logCursor.close(); - Collections.sort(downloadLog, new DownloadStatusComparator()); - return downloadLog; - } - - /** - * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over - * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)} if only metadata about - * the FeedItems is needed. - * - * @param context A context that is used for opening a database connection. - * @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title. - */ - public static List getFeedStatisticsList(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - List result = new ArrayList(); - Cursor cursor = adapter.getFeedStatisticsCursor(); - if (cursor.moveToFirst()) { - do { - result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED), - cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS), - cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS), - cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES), - new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE)))); - } while (cursor.moveToNext()); - } - - cursor.close(); - adapter.close(); - return result; - } - - /** - * Loads a specific Feed from the database. - * - * @param context A context that is used for opening a database connection. - * @param feedId The ID of the Feed - * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the - * database and the items-attribute will be set correctly. - */ - public static Feed getFeed(final Context context, final long feedId) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Feed result = getFeed(context, feedId, adapter); - adapter.close(); - return result; - } - - static Feed getFeed(final Context context, final long feedId, PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading feed with id " + feedId); - Feed feed = null; - - Cursor feedCursor = adapter.getFeedCursor(feedId); - if (feedCursor.moveToFirst()) { - feed = extractFeedFromCursorRow(adapter, feedCursor); - feed.setItems(getFeedItemList(context, feed)); - } else { - Log.e(TAG, "getFeed could not find feed with id " + feedId); - } - feedCursor.close(); - return feed; - } - - static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading feeditem with id " + itemId); - FeedItem item = null; - - Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId)); - if (itemCursor.moveToFirst()) { - List list = extractItemlistFromCursor(adapter, itemCursor); - if (list.size() > 0) { - item = list.get(0); - loadFeedDataOfFeedItemlist(context, list); - } - } - return item; - - } - - /** - * Loads a specific FeedItem from the database. - * - * @param context A context that is used for opening a database connection. - * @param itemId The ID of the FeedItem - * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes of the FeedItem will - * also be loaded from the database. - */ - public static FeedItem getFeedItem(final Context context, final long itemId) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading feeditem with id " + itemId); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - FeedItem item = getFeedItem(context, itemId, adapter); - adapter.close(); - return item; - - } - - /** - * Loads additional information about a FeedItem, e.g. shownotes - * - * @param context A context that is used for opening a database connection. - * @param item The FeedItem - */ - public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor extraCursor = adapter.getExtraInformationOfItem(item); - if (extraCursor.moveToFirst()) { - String description = extraCursor - .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION); - String contentEncoded = extraCursor - .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED); - item.setDescription(description); - item.setContentEncoded(contentEncoded); - } - adapter.close(); - } - - /** - * Returns the number of downloaded episodes. - * - * @param context A context that is used for opening a database connection. - * @return The number of downloaded episodes. - */ - public static int getNumberOfDownloadedEpisodes(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final int result = adapter.getNumberOfDownloadedEpisodes(); - adapter.close(); - return result; - } - - /** - * Returns the number of unread items. - * - * @param context A context that is used for opening a database connection. - * @return The number of unread items. - */ - public static int getNumberOfUnreadItems(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final int result = adapter.getNumberOfUnreadItems(); - adapter.close(); - return result; - } - - /** - * Searches the DB for a FeedImage of the given id. - * - * @param context A context that is used for opening a database connection. - * @param imageId The id of the object - * @return The found object - */ - public static FeedImage getFeedImage(final Context context, final long imageId) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - FeedImage result = getFeedImage(adapter, imageId); - adapter.close(); - return result; - } - - /** - * Searches the DB for a FeedImage of the given id. - * - * @param id The id of the object - * @return The found object - */ - static FeedImage getFeedImage(PodDBAdapter adapter, final long id) { - Cursor cursor = adapter.getImageCursor(id); - if ((cursor.getCount() == 0) || !cursor.moveToFirst()) { - throw new SQLException("No FeedImage found at index: " + id); - } - FeedImage image = new FeedImage(id, cursor.getString(cursor - .getColumnIndex(PodDBAdapter.KEY_TITLE)), - cursor.getString(cursor - .getColumnIndex(PodDBAdapter.KEY_FILE_URL)), - cursor.getString(cursor - .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)), - cursor.getInt(cursor - .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0 - ); - cursor.close(); - return image; - } - - /** - * Searches the DB for a FeedMedia of the given id. - * - * @param context A context that is used for opening a database connection. - * @param mediaId The id of the object - * @return The found object - */ - public static FeedMedia getFeedMedia(final Context context, final long mediaId) { - PodDBAdapter adapter = new PodDBAdapter(context); - - adapter.open(); - Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId); - - FeedMedia media = null; - if (mediaCursor.moveToFirst()) { - final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); - media = extractFeedMediaFromCursorRow(mediaCursor); - FeedItem item = getFeedItem(context, itemId); - if (media != null && item != null) { - media.setItem(item); - item.setMedia(media); - } - } - - mediaCursor.close(); - adapter.close(); - - return media; - } - - /** - * Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems. - * - * @param context A context that is used for opening a database connection. - * @return The flattr queue as a List. - */ - public static List getFlattrQueue(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - List result = new ArrayList(); - - // load feeds - Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor(); - if (feedCursor.moveToFirst()) { - do { - result.add(extractFeedFromCursorRow(adapter, feedCursor)); - } while (feedCursor.moveToNext()); - } - feedCursor.close(); - - //load feed items - Cursor feedItemCursor = adapter.getFeedItemsInFlattrQueueCursor(); - result.addAll(extractItemlistFromCursor(adapter, feedItemCursor)); - feedItemCursor.close(); - - adapter.close(); - Log.d(TAG, "Returning flattrQueueIterator for queue with " + result.size() + " items."); - return result; - } - - - /** - * Returns true if the flattr queue is empty. - * - * @param context A context that is used for opening a database connection. - */ - public static boolean getFlattrQueueEmpty(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - boolean empty = adapter.getFlattrQueueSize() == 0; - adapter.close(); - return empty; - } - - /** - * Returns data necessary for displaying the navigation drawer. This includes - * the list of subscriptions, the number of items in the queue and the number of unread - * items. - * - * @param context A context that is used for opening a database connection. - */ - public static NavDrawerData getNavDrawerData(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - List feeds = getFeedList(adapter); - int queueSize = adapter.getQueueSize(); - int numUnreadItems = adapter.getNumberOfUnreadItems(); - NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems); - adapter.close(); - return result; - } - - public static class NavDrawerData { - public List feeds; - public int queueSize; - public int numUnreadItems; - - public NavDrawerData(List feeds, int queueSize, int numUnreadItems) { - this.feeds = feeds; - this.queueSize = queueSize; - this.numUnreadItems = numUnreadItems; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/DBTasks.java b/app/src/main/java/de/danoeh/antennapod/storage/DBTasks.java deleted file mode 100644 index a230ba797..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/DBTasks.java +++ /dev/null @@ -1,895 +0,0 @@ -package de.danoeh.antennapod.storage; - -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.asynctask.FlattrClickWorker; -import de.danoeh.antennapod.asynctask.FlattrStatusFetcher; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.GpodnetSyncService; -import de.danoeh.antennapod.service.download.DownloadStatus; -import de.danoeh.antennapod.service.playback.PlaybackService; -import de.danoeh.antennapod.util.DownloadError; -import de.danoeh.antennapod.util.NetworkUtils; -import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.util.exception.MediaFileNotFoundException; -import de.danoeh.antennapod.util.flattr.FlattrUtils; - -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Provides methods for doing common tasks that use DBReader and DBWriter. - */ -public final class DBTasks { - private static final String TAG = "DBTasks"; - - /** - * Executor service used by the autodownloadUndownloadedEpisodes method. - */ - private static ExecutorService autodownloadExec; - - static { - autodownloadExec = Executors.newSingleThreadExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - } - - private DBTasks() { - } - - /** - * Removes the feed with the given download url. This method should NOT be executed on the GUI thread. - * - * @param context Used for accessing the db - * @param downloadUrl URL of the feed. - */ - public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor cursor = adapter.getFeedCursorDownloadUrls(); - long feedID = 0; - if (cursor.moveToFirst()) { - do { - if (cursor.getString(1).equals(downloadUrl)) { - feedID = cursor.getLong(0); - } - } while (cursor.moveToNext()); - } - cursor.close(); - adapter.close(); - - if (feedID != 0) { - try { - DBWriter.deleteFeed(context, feedID).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } else { - Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl); - } - } - - /** - * Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to - * start the {@link PlaybackService}. - * - * @param context Used for sending starting Services and Activities. - * @param media The FeedMedia object. - * @param showPlayer If true, starts the appropriate player activity ({@link de.danoeh.antennapod.activity.AudioplayerActivity} - * or {@link de.danoeh.antennapod.activity.VideoplayerActivity} - * @param startWhenPrepared Parameter for the {@link PlaybackService} start intent. If true, playback will start as - * soon as the PlaybackService has finished loading the FeedMedia object's file. - * @param shouldStream Parameter for the {@link PlaybackService} start intent. If true, the FeedMedia object's file - * will be streamed, otherwise the downloaded file will be used. If the downloaded file cannot be - * found, the PlaybackService will shutdown and the database entry of the FeedMedia object will be - * corrected. - */ - public static void playMedia(final Context context, final FeedMedia media, - boolean showPlayer, boolean startWhenPrepared, boolean shouldStream) { - try { - if (!shouldStream) { - if (media.fileExists() == false) { - throw new MediaFileNotFoundException( - "No episode was found at " + media.getFile_url(), - media); - } - } - // Start playback Service - Intent launchIntent = new Intent(context, PlaybackService.class); - launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, - startWhenPrepared); - launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, - shouldStream); - launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, - true); - context.startService(launchIntent); - if (showPlayer) { - // Launch media player - context.startActivity(PlaybackService.getPlayerActivityIntent( - context, media)); - } - DBWriter.addQueueItemAt(context, media.getItem().getId(), 0, false); - } catch (MediaFileNotFoundException e) { - e.printStackTrace(); - if (media.isPlaying()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - notifyMissingFeedMediaFile(context, media); - } - } - - private static AtomicBoolean isRefreshing = new AtomicBoolean(false); - - /** - * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still - * enqueuing Feeds for download from a previous call - * - * @param context Might be used for accessing the database - * @param feeds List of Feeds that should be refreshed. - */ - public static void refreshAllFeeds(final Context context, - final List feeds) { - if (isRefreshing.compareAndSet(false, true)) { - new Thread() { - public void run() { - if (feeds != null) { - refreshFeeds(context, feeds); - } else { - refreshFeeds(context, DBReader.getFeedList(context)); - } - isRefreshing.set(false); - - if (FlattrUtils.hasToken()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Flattring all pending things."); - new FlattrClickWorker(context).executeAsync(); // flattr pending things - - if (BuildConfig.DEBUG) Log.d(TAG, "Fetching flattr status."); - new FlattrStatusFetcher(context).start(); - - } - GpodnetSyncService.sendSyncIntent(context); - autodownloadUndownloadedItems(context); - } - }.start(); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Ignoring request to refresh all feeds: Refresh lock is locked"); - } - } - - /** - * Used by refreshExpiredFeeds to determine which feeds should be refreshed. - * This method will use the value specified in the UserPreferences as the - * expiration time. - * - * @param context Used for DB access. - * @return A list of expired feeds. An empty list will be returned if there - * are no expired feeds. - */ - public static List getExpiredFeeds(final Context context) { - long millis = UserPreferences.getUpdateInterval(); - - if (millis > 0) { - - List feedList = DBReader.getExpiredFeedsList(context, - millis); - if (feedList.size() > 0) { - refreshFeeds(context, feedList); - } - return feedList; - } else { - return new ArrayList(); - } - } - - /** - * Refreshes expired Feeds in the list returned by the getExpiredFeedsList(Context, long) method in DBReader. - * The expiration date parameter is determined by the update interval specified in {@link UserPreferences}. - * - * @param context Used for DB access. - */ - public static void refreshExpiredFeeds(final Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Refreshing expired feeds"); - - new Thread() { - public void run() { - refreshFeeds(context, getExpiredFeeds(context)); - } - }.start(); - } - - private static void refreshFeeds(final Context context, - final List feedList) { - - for (Feed feed : feedList) { - try { - refreshFeed(context, feed); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - context, - new DownloadStatus(feed, feed - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, false, e - .getMessage() - ) - ); - } - } - - } - - /** - * Updates a specific Feed. - * - * @param context Used for requesting the download. - * @param feed The Feed object. - */ - public static void refreshFeed(Context context, Feed feed) - throws DownloadRequestException { - Feed f; - if (feed.getPreferences() == null) { - f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle()); - } else { - f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle(), - feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); - } - f.setId(feed.getId()); - DownloadRequester.getInstance().downloadFeed(context, f); - } - - /** - * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file. - * - * @param context Used for requesting the download. - * @param image The FeedImage object. - */ - public static void notifyInvalidImageFile(final Context context, - final FeedImage image) { - Log.i(TAG, - "The DB was notified about an invalid image download. It will now try to re-download the image file"); - try { - DownloadRequester.getInstance().downloadImage(context, image); - } catch (DownloadRequestException e) { - e.printStackTrace(); - Log.w(TAG, "Failed to download invalid feed image"); - } - } - - /** - * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the - * DB and send a FeedUpdateBroadcast. - */ - public static void notifyMissingFeedMediaFile(final Context context, - final FeedMedia media) { - Log.i(TAG, - "The feedmanager was notified about a missing episode. It will update its database now."); - media.setDownloaded(false); - media.setFile_url(null); - DBWriter.setFeedMedia(context, media); - EventDistributor.getInstance().sendFeedUpdateBroadcast(); - } - - /** - * Request the download of all objects in the queue. from a separate Thread. - * - * @param context Used for requesting the download an accessing the database. - */ - public static void downloadAllItemsInQueue(final Context context) { - new Thread() { - public void run() { - List queue = DBReader.getQueue(context); - if (!queue.isEmpty()) { - try { - downloadFeedItems(context, - queue.toArray(new FeedItem[queue.size()])); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - } - }.start(); - } - - /** - * Requests the download of a list of FeedItem objects. - * - * @param context Used for requesting the download and accessing the DB. - * @param items The FeedItem objects. - */ - public static void downloadFeedItems(final Context context, - FeedItem... items) throws DownloadRequestException { - downloadFeedItems(true, context, items); - } - - private static void downloadFeedItems(boolean performAutoCleanup, - final Context context, final FeedItem... items) - throws DownloadRequestException { - final DownloadRequester requester = DownloadRequester.getInstance(); - - if (performAutoCleanup) { - new Thread() { - - @Override - public void run() { - performAutoCleanup(context, - getPerformAutoCleanupArgs(context, items.length)); - } - - }.start(); - } - for (FeedItem item : items) { - if (item.getMedia() != null - && !requester.isDownloadingFile(item.getMedia()) - && !item.getMedia().isDownloaded()) { - if (items.length > 1) { - try { - requester.downloadMedia(context, item.getMedia()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus(context, - new DownloadStatus(item.getMedia(), item - .getMedia() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage() - ) - ); - } - } else { - requester.downloadMedia(context, item.getMedia()); - } - } - } - } - - private static int getNumberOfUndownloadedEpisodes( - final List queue, final List unreadItems) { - int counter = 0; - for (FeedItem item : queue) { - if (item.hasMedia() && !item.getMedia().isDownloaded() - && !item.getMedia().isPlaying() - && item.getFeed().getPreferences().getAutoDownload()) { - counter++; - } - } - for (FeedItem item : unreadItems) { - if (item.hasMedia() && !item.getMedia().isDownloaded() - && item.getFeed().getPreferences().getAutoDownload()) { - counter++; - } - } - return counter; - } - - /** - * Looks for undownloaded episodes in the queue or list of unread items and request a download if - * 1. Network is available - * 2. There is free space in the episode cache - * This method is executed on an internal single thread executor. - * - * @param context Used for accessing the DB. - * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if - * its media ID is in the mediaIds list. - * @return A Future that can be used for waiting for the methods completion. - */ - public static Future autodownloadUndownloadedItems(final Context context, final long... mediaIds) { - return autodownloadExec.submit(new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Performing auto-dl of undownloaded episodes"); - if (NetworkUtils.autodownloadNetworkAvailable(context) - && UserPreferences.isEnableAutodownload()) { - final List queue = DBReader.getQueue(context); - final List unreadItems = DBReader - .getUnreadItemsList(context); - - int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue, - unreadItems); - int downloadedEpisodes = DBReader - .getNumberOfDownloadedEpisodes(context); - int deletedEpisodes = performAutoCleanup(context, - getPerformAutoCleanupArgs(context, undownloadedEpisodes)); - int episodeSpaceLeft = undownloadedEpisodes; - boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences - .getEpisodeCacheSizeUnlimited(); - - if (!cacheIsUnlimited - && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes - + undownloadedEpisodes) { - episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() - - (downloadedEpisodes - deletedEpisodes); - } - - Arrays.sort(mediaIds); // sort for binary search - final boolean ignoreMediaIds = mediaIds.length == 0; - List itemsToDownload = new ArrayList(); - - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (int i = 0; i < queue.size(); i++) { // ignore playing item - FeedItem item = queue.get(i); - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; - if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) - && item.hasMedia() - && !item.getMedia().isDownloaded() - && !item.getMedia().isPlaying() - && item.getFeed().getPreferences().getAutoDownload()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } - - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (FeedItem item : unreadItems) { - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; - if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) - && item.hasMedia() - && !item.getMedia().isDownloaded() - && item.getFeed().getPreferences().getAutoDownload()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Enqueueing " + itemsToDownload.size() - + " items for download"); - - try { - downloadFeedItems(false, context, - itemsToDownload.toArray(new FeedItem[itemsToDownload - .size()]) - ); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - - } - } - }); - - } - - private static int getPerformAutoCleanupArgs(Context context, - final int episodeNumber) { - if (episodeNumber >= 0 - && UserPreferences.getEpisodeCacheSize() != UserPreferences - .getEpisodeCacheSizeUnlimited()) { - int downloadedEpisodes = DBReader - .getNumberOfDownloadedEpisodes(context); - if (downloadedEpisodes + episodeNumber >= UserPreferences - .getEpisodeCacheSize()) { - - return downloadedEpisodes + episodeNumber - - UserPreferences.getEpisodeCacheSize(); - } - } - return 0; - } - - /** - * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller - * 'playbackCompletionDate'-value will be deleted first. - *

- * This method should NOT be executed on the GUI thread. - * - * @param context Used for accessing the DB. - */ - public static void performAutoCleanup(final Context context) { - performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0)); - } - - private static int performAutoCleanup(final Context context, - final int episodeNumber) { - List candidates = new ArrayList(); - List downloadedItems = DBReader.getDownloadedItems(context); - QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context)); - List delete; - for (FeedItem item : downloadedItems) { - if (item.hasMedia() && item.getMedia().isDownloaded() - && !queue.contains(item.getId()) && item.isRead()) { - candidates.add(item); - } - - } - - Collections.sort(candidates, new Comparator() { - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - Date l = lhs.getMedia().getPlaybackCompletionDate(); - Date r = rhs.getMedia().getPlaybackCompletionDate(); - - if (l == null) { - l = new Date(0); - } - if (r == null) { - r = new Date(0); - } - return l.compareTo(r); - } - }); - - if (candidates.size() > episodeNumber) { - delete = candidates.subList(0, episodeNumber); - } else { - delete = candidates; - } - - for (FeedItem item : delete) { - try { - DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - int counter = delete.size(); - - if (BuildConfig.DEBUG) - Log.d(TAG, String.format( - "Auto-delete deleted %d episodes (%d requested)", counter, - episodeNumber)); - - return counter; - } - - /** - * Adds all FeedItem objects whose 'read'-attribute is false to the queue in a separate thread. - */ - public static void enqueueAllNewItems(final Context context) { - long[] unreadItems = DBReader.getUnreadItemIds(context); - DBWriter.addQueueItem(context, unreadItems); - } - - /** - * Returns the successor of a FeedItem in the queue. - * - * @param context Used for accessing the DB. - * @param itemId ID of the FeedItem - * @param queue Used for determining the successor of the item. If this parameter is null, the method will load - * the queue from the database in the same thread. - * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor. - */ - public static FeedItem getQueueSuccessorOfItem(Context context, - final long itemId, List queue) { - FeedItem result = null; - if (queue == null) { - queue = DBReader.getQueue(context); - } - if (queue != null) { - Iterator iterator = queue.iterator(); - while (iterator.hasNext()) { - FeedItem item = iterator.next(); - if (item.getId() == itemId) { - if (iterator.hasNext()) { - result = iterator.next(); - } - break; - } - } - } - return result; - } - - /** - * Loads the queue from the database and checks if the specified FeedItem is in the queue. - * This method should NOT be executed in the GUI thread. - * - * @param context Used for accessing the DB. - * @param feedItemId ID of the FeedItem - */ - public static boolean isInQueue(Context context, final long feedItemId) { - List queue = DBReader.getQueueIDList(context); - return QueueAccess.IDListAccess(queue).contains(feedItemId); - } - - private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter, - Feed feed) { - if (feed.getId() != 0) { - return DBReader.getFeed(context, feed.getId(), adapter); - } else { - List feeds = DBReader.getFeedList(context); - for (Feed f : feeds) { - if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { - f.setItems(DBReader.getFeedItemList(context, f)); - return f; - } - } - } - return null; - } - - /** - * Get a FeedItem by its identifying value. - */ - private static FeedItem searchFeedItemByIdentifyingValue(Feed feed, - String identifier) { - for (FeedItem item : feed.getItems()) { - if (item.getIdentifyingValue().equals(identifier)) { - return item; - } - } - return null; - } - - /** - * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same - * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed. - * These FeedItems will be marked as unread. - *

- * This method can update multiple feeds at once. Submitting a feed twice in the same method call can result in undefined behavior. - *

- * This method should NOT be executed on the GUI thread. - * - * @param context Used for accessing the DB. - * @param newFeeds The new Feed objects. - * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise. - */ - public static synchronized Feed[] updateFeed(final Context context, - final Feed... newFeeds) { - List newFeedsList = new ArrayList(); - List updatedFeedsList = new ArrayList(); - Feed[] resultFeeds = new Feed[newFeeds.length]; - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - - for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) { - - final Feed newFeed = newFeeds[feedIdx]; - - // Look up feed in the feedslist - final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter, - newFeed); - if (savedFeed == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Found no existing Feed with title " - + newFeed.getTitle() + ". Adding as new one." - ); - // Add a new Feed - newFeedsList.add(newFeed); - resultFeeds[feedIdx] = newFeed; - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed with title " + newFeed.getTitle() - + " already exists. Syncing new with existing one."); - - Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); - if (savedFeed.compareWithOther(newFeed)) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Feed has updated attribute values. Updating old feed's attributes"); - savedFeed.updateFromOther(newFeed); - } - if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); - savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); - } - // Look for new or updated Items - for (int idx = 0; idx < newFeed.getItems().size(); idx++) { - final FeedItem item = newFeed.getItems().get(idx); - FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, - item.getIdentifyingValue()); - if (oldItem == null) { - // item is new - final int i = idx; - item.setFeed(savedFeed); - savedFeed.getItems().add(i, item); - item.setRead(false); - } else { - oldItem.updateFromOther(item); - } - } - // update attributes - savedFeed.setLastUpdate(newFeed.getLastUpdate()); - savedFeed.setType(newFeed.getType()); - - updatedFeedsList.add(savedFeed); - resultFeeds[feedIdx] = savedFeed; - } - } - - adapter.close(); - - try { - DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get(); - DBWriter.setCompleteFeed(context, updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - - EventDistributor.getInstance().sendFeedUpdateBroadcast(); - - return resultFeeds; - } - - /** - * Searches the titles of FeedItems of a specific Feed for a given - * string. - * - * @param context Used for accessing the DB. - * @param feedID The id of the feed whose items should be searched. - * @param query The search string. - * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. - */ - public static FutureTask> searchFeedItemTitle(final Context context, - final long feedID, final String query) { - return new FutureTask>(new QueryTask>(context) { - @Override - public void execute(PodDBAdapter adapter) { - Cursor searchResult = adapter.searchItemTitles(feedID, - query); - List items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); - setResult(items); - searchResult.close(); - } - }); - } - - /** - * Searches the descriptions of FeedItems of a specific Feed for a given - * string. - * - * @param context Used for accessing the DB. - * @param feedID The id of the feed whose items should be searched. - * @param query The search string - * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. - */ - public static FutureTask> searchFeedItemDescription(final Context context, - final long feedID, final String query) { - return new FutureTask>(new QueryTask>(context) { - @Override - public void execute(PodDBAdapter adapter) { - Cursor searchResult = adapter.searchItemDescriptions(feedID, - query); - List items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); - setResult(items); - searchResult.close(); - } - }); - } - - /** - * Searches the contentEncoded-value of FeedItems of a specific Feed for a given - * string. - * - * @param context Used for accessing the DB. - * @param feedID The id of the feed whose items should be searched. - * @param query The search string - * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. - */ - public static FutureTask> searchFeedItemContentEncoded(final Context context, - final long feedID, final String query) { - return new FutureTask>(new QueryTask>(context) { - @Override - public void execute(PodDBAdapter adapter) { - Cursor searchResult = adapter.searchItemContentEncoded(feedID, - query); - List items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); - setResult(items); - searchResult.close(); - } - }); - } - - /** - * Searches chapters of the FeedItems of a specific Feed for a given string. - * - * @param context Used for accessing the DB. - * @param feedID The id of the feed whose items should be searched. - * @param query The search string - * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. - */ - public static FutureTask> searchFeedItemChapters(final Context context, - final long feedID, final String query) { - return new FutureTask>(new QueryTask>(context) { - @Override - public void execute(PodDBAdapter adapter) { - Cursor searchResult = adapter.searchItemChapters(feedID, - query); - List items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); - setResult(items); - searchResult.close(); - } - }); - } - - /** - * A runnable which should be used for database queries. The onCompletion - * method is executed on the database executor to handle Cursors correctly. - * This class automatically creates a PodDBAdapter object and closes it when - * it is no longer in use. - */ - static abstract class QueryTask implements Callable { - private T result; - private Context context; - - public QueryTask(Context context) { - this.context = context; - } - - @Override - public T call() throws Exception { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - execute(adapter); - adapter.close(); - return result; - } - - public abstract void execute(PodDBAdapter adapter); - - protected void setResult(T result) { - this.result = result; - } - } - - /** - * Adds the given FeedItem to the flattr queue if the user is logged in. Otherwise, a dialog - * will be opened that lets the user go either to the login screen or the website of the flattr thing. - * - * @param context - * @param item - */ - public static void flattrItemIfLoggedIn(Context context, FeedItem item) { - if (FlattrUtils.hasToken()) { - item.getFlattrStatus().setFlattrQueue(); - DBWriter.setFlattredStatus(context, item, true); - } else { - FlattrUtils.showNoTokenDialogOrRedirect(context, item.getPaymentLink()); - } - } - - /** - * Adds the given Feed to the flattr queue if the user is logged in. Otherwise, a dialog - * will be opened that lets the user go either to the login screen or the website of the flattr thing. - * - * @param context - * @param feed - */ - public static void flattrFeedIfLoggedIn(Context context, Feed feed) { - if (FlattrUtils.hasToken()) { - feed.getFlattrStatus().setFlattrQueue(); - DBWriter.setFlattredStatus(context, feed, true); - } else { - FlattrUtils.showNoTokenDialogOrRedirect(context, feed.getPaymentLink()); - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/DBWriter.java b/app/src/main/java/de/danoeh/antennapod/storage/DBWriter.java deleted file mode 100644 index 9916ac97f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/DBWriter.java +++ /dev/null @@ -1,974 +0,0 @@ -package de.danoeh.antennapod.storage; - -import android.app.backup.BackupManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.preference.PreferenceManager; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.asynctask.FlattrClickWorker; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.preferences.GpodnetPreferences; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.service.download.DownloadStatus; -import de.danoeh.antennapod.service.playback.PlaybackService; -import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.flattr.FlattrStatus; -import de.danoeh.antennapod.util.flattr.FlattrThing; -import de.danoeh.antennapod.util.flattr.SimpleFlattrThing; -import org.shredzone.flattr4j.model.Flattr; - -import java.io.File; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; - -/** - * Provides methods for writing data to AntennaPod's database. - * In general, DBWriter-methods will be executed on an internal ExecutorService. - * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's - * will NOT contain any results. - * The caller can also use the {@link EventDistributor} in order to be notified about the method's completion asynchronously. - * This class will use the {@link EventDistributor} to notify listeners about changes in the database. - */ -public class DBWriter { - private static final String TAG = "DBWriter"; - - private static final ExecutorService dbExec; - - static { - dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - } - - private DBWriter() { - } - - /** - * Deletes a downloaded FeedMedia file from the storage device. - * - * @param context A context that is used for opening a database connection. - * @param mediaId ID of the FeedMedia object whose downloaded file should be deleted. - */ - public static Future deleteFeedMediaOfItem(final Context context, - final long mediaId) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - - final FeedMedia media = DBReader.getFeedMedia(context, mediaId); - if (media != null) { - Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s", - media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded()))); - boolean result = false; - if (media.isDownloaded()) { - // delete downloaded media file - File mediaFile = new File(media.getFile_url()); - if (mediaFile.exists()) { - result = mediaFile.delete(); - } - media.setDownloaded(false); - media.setFile_url(null); - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setMedia(media); - adapter.close(); - - // If media is currently being played, change playback - // type to 'stream' and shutdown playback service - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) { - if (media.getId() == PlaybackPreferences - .getCurrentlyPlayingFeedMediaId()) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - true); - editor.commit(); - } - if (PlaybackPreferences - .getCurrentlyPlayingFeedMediaId() == media - .getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting File. Result: " + result); - EventDistributor.getInstance().sendQueueUpdateBroadcast(); - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); - } - } - }); - } - - /** - * Deletes a Feed and all downloaded files of its components like images and downloaded episodes. - * - * @param context A context that is used for opening a database connection. - * @param feedId ID of the Feed that should be deleted. - */ - public static Future deleteFeed(final Context context, final long feedId) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - DownloadRequester requester = DownloadRequester.getInstance(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context - .getApplicationContext()); - final Feed feed = DBReader.getFeed(context, feedId); - if (feed != null) { - if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA - && PlaybackPreferences.getLastPlayedFeedId() == feed - .getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - -1); - editor.commit(); - } - - // delete image file - if (feed.getImage() != null) { - if (feed.getImage().isDownloaded() - && feed.getImage().getFile_url() != null) { - File imageFile = new File(feed.getImage() - .getFile_url()); - imageFile.delete(); - } else if (requester.isDownloadingFile(feed.getImage())) { - requester.cancelDownload(context, feed.getImage()); - } - } - // delete stored media files and mark them as read - List queue = DBReader.getQueue(context); - boolean queueWasModified = false; - if (feed.getItems() == null) { - DBReader.getFeedItemList(context, feed); - } - - for (FeedItem item : feed.getItems()) { - queueWasModified |= queue.remove(item); - if (item.getMedia() != null - && item.getMedia().isDownloaded()) { - File mediaFile = new File(item.getMedia() - .getFile_url()); - mediaFile.delete(); - } else if (item.getMedia() != null - && requester.isDownloadingFile(item.getMedia())) { - requester.cancelDownload(context, item.getMedia()); - } - - if (item.hasItemImage()) { - FeedImage image = item.getImage(); - if (image.isDownloaded() && image.getFile_url() != null) { - File imgFile = new File(image.getFile_url()); - imgFile.delete(); - } else if (requester.isDownloadingFile(image)) { - requester.cancelDownload(context, item.getImage()); - } - } - } - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - if (queueWasModified) { - adapter.setQueue(queue); - } - adapter.removeFeed(feed); - adapter.close(); - - GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); - EventDistributor.getInstance().sendFeedUpdateBroadcast(); - - BackupManager backupManager = new BackupManager(context); - backupManager.dataChanged(); - } - } - }); - } - - /** - * Deletes the entire playback history. - * - * @param context A context that is used for opening a database connection. - */ - public static Future clearPlaybackHistory(final Context context) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.clearPlaybackHistory(); - adapter.close(); - EventDistributor.getInstance() - .sendPlaybackHistoryUpdateBroadcast(); - } - }); - } - - /** - * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if - * its playback completion date is set to a non-null value. This method will set the playback completion date to the - * current date regardless of the current value. - * - * @param context A context that is used for opening a database connection. - * @param media FeedMedia that should be added to the playback history. - */ - public static Future addItemToPlaybackHistory(final Context context, - final FeedMedia media) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Adding new item to playback history"); - media.setPlaybackCompletionDate(new Date()); - // reset played_duration to 0 so that it behaves correctly when the episode is played again - media.setPlayedDuration(0); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedMediaPlaybackCompletionDate(media); - adapter.close(); - EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); - - } - }); - } - - private static void cleanupDownloadLog(final PodDBAdapter adapter) { - final long logSize = adapter.getDownloadLogSize(); - if (logSize > DBReader.DOWNLOAD_LOG_SIZE) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Cleaning up download log"); - adapter.removeDownloadLogItems(logSize - DBReader.DOWNLOAD_LOG_SIZE); - } - } - - /** - * Adds a Download status object to the download log. - * - * @param context A context that is used for opening a database connection. - * @param status The DownloadStatus object. - */ - public static Future addDownloadStatus(final Context context, - final DownloadStatus status) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setDownloadStatus(status); - adapter.close(); - EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); - } - }); - - } - - /** - * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to - * true. If the FeedItem is already in the queue, the queue will not be modified. - * - * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem that should be added to the queue. - * @param index Destination index. Must be in range 0..queue.size() - * @param performAutoDownload True if an auto-download process should be started after the operation - * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size() - */ - public static Future addQueueItemAt(final Context context, final long itemId, - final int index, final boolean performAutoDownload) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List queue = DBReader - .getQueue(context, adapter); - FeedItem item = null; - - if (queue != null) { - boolean queueModified = false; - boolean unreadItemsModified = false; - - if (!itemListContains(queue, itemId)) { - item = DBReader.getFeedItem(context, itemId); - if (item != null) { - queue.add(index, item); - queueModified = true; - if (!item.isRead()) { - item.setRead(true); - unreadItemsModified = true; - } - } - } - if (queueModified) { - adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); - } - if (unreadItemsModified && item != null) { - adapter.setSingleFeedItem(item); - EventDistributor.getInstance() - .sendUnreadItemsUpdateBroadcast(); - } - } - adapter.close(); - if (performAutoDownload) { - DBTasks.autodownloadUndownloadedItems(context); - } - - } - }); - - } - - /** - * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. - * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. - * - * @param context A context that is used for opening a database connection. - * @param itemIds IDs of the FeedItem objects that should be added to the queue. - */ - public static Future addQueueItem(final Context context, - final long... itemIds) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - if (itemIds.length > 0) { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List queue = DBReader.getQueue(context, - adapter); - - if (queue != null) { - boolean queueModified = false; - boolean unreadItemsModified = false; - List itemsToSave = new LinkedList(); - for (int i = 0; i < itemIds.length; i++) { - if (!itemListContains(queue, itemIds[i])) { - final FeedItem item = DBReader.getFeedItem( - context, itemIds[i]); - - if (item != null) { - queue.add(item); - queueModified = true; - if (!item.isRead()) { - item.setRead(true); - itemsToSave.add(item); - unreadItemsModified = true; - } - } - } - } - if (queueModified) { - adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); - } - if (unreadItemsModified) { - adapter.setFeedItemlist(itemsToSave); - EventDistributor.getInstance() - .sendUnreadItemsUpdateBroadcast(); - } - } - adapter.close(); - DBTasks.autodownloadUndownloadedItems(context); - } - } - }); - - } - - /** - * Removes all FeedItem objects from the queue. - * - * @param context A context that is used for opening a database connection. - */ - public static Future clearQueue(final Context context) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.clearQueue(); - adapter.close(); - - EventDistributor.getInstance().sendQueueUpdateBroadcast(); - } - }); - } - - /** - * Removes a FeedItem object from the queue. - * - * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem that should be removed. - * @param performAutoDownload true if an auto-download process should be started after the operation. - */ - public static Future removeQueueItem(final Context context, - final long itemId, final boolean performAutoDownload) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List queue = DBReader - .getQueue(context, adapter); - FeedItem item = null; - - if (queue != null) { - boolean queueModified = false; - QueueAccess queueAccess = QueueAccess.ItemListAccess(queue); - if (queueAccess.contains(itemId)) { - item = DBReader.getFeedItem(context, itemId); - if (item != null) { - queueModified = queueAccess.remove(itemId); - } - } - if (queueModified) { - adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); - } else { - Log.w(TAG, "Queue was not modified by call to removeQueueItem"); - } - } else { - Log.e(TAG, "removeQueueItem: Could not load queue"); - } - adapter.close(); - if (performAutoDownload) { - DBTasks.autodownloadUndownloadedItems(context); - } - } - }); - - } - - /** - * Moves the specified item to the top of the queue. - * - * @param context A context that is used for opening a database connection. - * @param itemId The item to move to the top of the queue - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. - */ - public static Future moveQueueItemToTop(final Context context, final long itemId, final boolean broadcastUpdate) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - List queueIdList = DBReader.getQueueIDList(context); - int currentLocation = 0; - for (long id : queueIdList) { - if (id == itemId) { - moveQueueItemHelper(context, currentLocation, 0, broadcastUpdate); - return; - } - currentLocation++; - } - Log.e(TAG, "moveQueueItemToTop: item not found"); - } - }); - } - - /** - * Moves the specified item to the bottom of the queue. - * - * @param context A context that is used for opening a database connection. - * @param itemId The item to move to the bottom of the queue - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. - */ - public static Future moveQueueItemToBottom(final Context context, final long itemId, - final boolean broadcastUpdate) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - List queueIdList = DBReader.getQueueIDList(context); - int currentLocation = 0; - for (long id : queueIdList) { - if (id == itemId) { - moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1, - broadcastUpdate); - return; - } - currentLocation++; - } - Log.e(TAG, "moveQueueItemToBottom: item not found"); - } - }); - } - - /** - * Changes the position of a FeedItem in the queue. - * - * @param context A context that is used for opening a database connection. - * @param from Source index. Must be in range 0..queue.size()-1. - * @param to Destination index. Must be in range 0..queue.size()-1. - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. - * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) - */ - public static Future moveQueueItem(final Context context, final int from, - final int to, final boolean broadcastUpdate) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - moveQueueItemHelper(context, from, to, broadcastUpdate); - } - }); - } - - /** - * Changes the position of a FeedItem in the queue. - *

- * This function must be run using the ExecutorService (dbExec). - * - * @param context A context that is used for opening a database connection. - * @param from Source index. Must be in range 0..queue.size()-1. - * @param to Destination index. Must be in range 0..queue.size()-1. - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. - * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) - */ - private static void moveQueueItemHelper(final Context context, final int from, - final int to, final boolean broadcastUpdate) { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List queue = DBReader - .getQueue(context, adapter); - - if (queue != null) { - if (from >= 0 && from < queue.size() && to >= 0 - && to < queue.size()) { - - final FeedItem item = queue.remove(from); - queue.add(to, item); - - adapter.setQueue(queue); - if (broadcastUpdate) { - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); - } - - } - } else { - Log.e(TAG, "moveQueueItemHelper: Could not load queue"); - } - adapter.close(); - } - - /** - * Sets the 'read'-attribute of a FeedItem to the specified value. - * - * @param context A context that is used for opening a database connection. - * @param item The FeedItem object - * @param read New value of the 'read'-attribute - * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object. - * If the FeedItem has no FeedMedia object, this parameter will be ignored. - */ - public static Future markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) { - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0; - return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition); - } - - /** - * Sets the 'read'-attribute of a FeedItem to the specified value. - * - * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem - * @param read New value of the 'read'-attribute - */ - public static Future markItemRead(final Context context, final long itemId, - final boolean read) { - return markItemRead(context, itemId, read, 0, false); - } - - private static Future markItemRead(final Context context, final long itemId, - final boolean read, final long mediaId, - final boolean resetMediaPosition) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedItemRead(read, itemId, mediaId, - resetMediaPosition); - adapter.close(); - - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); - } - }); - } - - /** - * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. - * - * @param context A context that is used for opening a database connection. - * @param feedId ID of the Feed. - */ - public static Future markFeedRead(final Context context, final long feedId) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId); - long[] itemIds = new long[itemCursor.getCount()]; - itemCursor.moveToFirst(); - for (int i = 0; i < itemIds.length; i++) { - itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - itemCursor.moveToNext(); - } - itemCursor.close(); - adapter.setFeedItemRead(true, itemIds); - adapter.close(); - - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); - } - }); - - } - - /** - * Sets the 'read'-attribute of all FeedItems to true. - * - * @param context A context that is used for opening a database connection. - */ - public static Future markAllItemsRead(final Context context) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor itemCursor = adapter.getUnreadItemsCursor(); - long[] itemIds = new long[itemCursor.getCount()]; - itemCursor.moveToFirst(); - for (int i = 0; i < itemIds.length; i++) { - itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - itemCursor.moveToNext(); - } - itemCursor.close(); - adapter.setFeedItemRead(true, itemIds); - adapter.close(); - - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); - } - }); - - } - - static Future addNewFeed(final Context context, final Feed... feeds) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setCompleteFeed(feeds); - adapter.close(); - - for (Feed feed : feeds) { - GpodnetPreferences.addAddedFeed(feed.getDownload_url()); - } - - BackupManager backupManager = new BackupManager(context); - backupManager.dataChanged(); - } - }); - } - - static Future setCompleteFeed(final Context context, final Feed... feeds) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setCompleteFeed(feeds); - adapter.close(); - - } - }); - - } - - /** - * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The - * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved. - * - * @param context A context that is used for opening a database connection. - * @param media The FeedMedia object. - */ - public static Future setFeedMedia(final Context context, - final FeedMedia media) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setMedia(media); - adapter.close(); - } - }); - } - - /** - * Saves the 'position' and 'duration' attributes of a FeedMedia object - * - * @param context A context that is used for opening a database connection. - * @param media The FeedMedia object. - */ - public static Future setFeedMediaPlaybackInformation(final Context context, final FeedMedia media) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedMediaPlaybackInformation(media); - adapter.close(); - } - }); - } - - /** - * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including - * the content of FeedComponent-attributes. - * - * @param context A context that is used for opening a database connection. - * @param item The FeedItem object. - */ - public static Future setFeedItem(final Context context, - final FeedItem item) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setSingleFeedItem(item); - adapter.close(); - } - }); - } - - /** - * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The - * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved. - * - * @param context A context that is used for opening a database connection. - * @param image The FeedImage object. - */ - public static Future setFeedImage(final Context context, - final FeedImage image) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setImage(image); - adapter.close(); - } - }); - } - - /** - * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed - * and the value is the updated URL - */ - public static Future updateFeedDownloadURLs(final Context context, final Map urls) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - for (String key : urls.keySet()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key)); - - adapter.setFeedDownloadUrl(key, urls.get(key)); - } - adapter.close(); - } - }); - } - - /** - * Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0. - * - * @param context Used for opening a database connection. - * @param preferences The FeedPreferences object. - */ - public static Future setFeedPreferences(final Context context, final FeedPreferences preferences) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedPreferences(preferences); - adapter.close(); - EventDistributor.getInstance().sendFeedUpdateBroadcast(); - } - }); - } - - private static boolean itemListContains(List items, long itemId) { - for (FeedItem item : items) { - if (item.getId() == itemId) { - return true; - } - } - return false; - } - - /** - * Saves the FlattrStatus of a FeedItem object in the database. - * - * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved - */ - public static Future setFeedItemFlattrStatus(final Context context, - final FeedItem item, - final boolean startFlattrClickWorker) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedItemFlattrStatus(item); - adapter.close(); - if (startFlattrClickWorker) { - new FlattrClickWorker(context).executeAsync(); - } - } - }); - } - - /** - * Saves the FlattrStatus of a Feed object in the database. - * - * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved - */ - private static Future setFeedFlattrStatus(final Context context, - final Feed feed, - final boolean startFlattrClickWorker) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedFlattrStatus(feed); - adapter.close(); - if (startFlattrClickWorker) { - new FlattrClickWorker(context).executeAsync(); - } - } - }); - } - - /** - * format an url for querying the database - * (postfix a / and apply percent-encoding) - */ - private static String formatURIForQuery(String uri) { - try { - return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8"); - } catch (UnsupportedEncodingException e) { - Log.e(TAG, e.getMessage()); - return ""; - } - } - - - /** - * Set flattr status of the passed thing (either a FeedItem or a Feed) - * - * @param context - * @param thing - * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved - * @return - */ - public static Future setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) { - // must propagate this to back db - if (thing instanceof FeedItem) - return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker); - else if (thing instanceof Feed) - return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker); - else if (thing instanceof SimpleFlattrThing) { - } // SimpleFlattrThings are generated on the fly and do not have DB backing - else - Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing"); - - return null; - } - - /** - * Reset flattr status to unflattrd for all items - */ - public static Future clearAllFlattrStatus(final Context context) { - Log.d(TAG, "clearAllFlattrStatus()"); - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.clearAllFlattrStatus(); - adapter.close(); - } - }); - } - - /** - * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp, - * where the information has been retrieved from the flattr API - */ - public static Future setFlattredStatus(final Context context, final List flattrList) { - Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items"); - // clear flattr status in db - clearAllFlattrStatus(context); - - // submit list with flattred things having normalized URLs to db - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - for (Flattr flattr : flattrList) { - adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime())); - } - adapter.close(); - } - }); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/DownloadRequestException.java b/app/src/main/java/de/danoeh/antennapod/storage/DownloadRequestException.java deleted file mode 100644 index 0ef766e58..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/DownloadRequestException.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.danoeh.antennapod.storage; - -/** - * Thrown by the DownloadRequester if a download request contains invalid data - * or something went wrong while processing the request. - */ -public class DownloadRequestException extends Exception { - - public DownloadRequestException() { - super(); - } - - public DownloadRequestException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public DownloadRequestException(String detailMessage) { - super(detailMessage); - } - - public DownloadRequestException(Throwable throwable) { - super(throwable); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/DownloadRequester.java b/app/src/main/java/de/danoeh/antennapod/storage/DownloadRequester.java deleted file mode 100644 index d305c572b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/DownloadRequester.java +++ /dev/null @@ -1,367 +0,0 @@ -package de.danoeh.antennapod.storage; - -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import android.webkit.URLUtil; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.*; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.download.DownloadRequest; -import de.danoeh.antennapod.service.download.DownloadService; -import de.danoeh.antennapod.util.FileNameGenerator; -import de.danoeh.antennapod.util.URLChecker; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.StringEscapeUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; - -import java.io.File; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - - -/** - * Sends download requests to the DownloadService. This class should always be used for starting downloads, - * otherwise they won't work correctly. - */ -public class DownloadRequester { - private static final String TAG = "DownloadRequester"; - - public static final String IMAGE_DOWNLOADPATH = "images/"; - public static final String FEED_DOWNLOADPATH = "cache/"; - public static final String MEDIA_DOWNLOADPATH = "media/"; - - private static DownloadRequester downloader; - - Map downloads; - - private DownloadRequester() { - downloads = new ConcurrentHashMap(); - } - - public static synchronized DownloadRequester getInstance() { - if (downloader == null) { - downloader = new DownloadRequester(); - } - return downloader; - } - - /** - * Starts a new download with the given DownloadRequest. This method should only - * be used from outside classes if the DownloadRequest was created by the DownloadService to - * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead. - * - * @param context Context object for starting the DownloadService - * @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method - * call will return false. - * @return True if the download request was accepted, false otherwise. - */ - public boolean download(Context context, DownloadRequest request) { - Validate.notNull(context); - Validate.notNull(request); - - if (downloads.containsKey(request.getSource())) { - if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); - return false; - } - downloads.put(request.getSource(), request); - - Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); - context.startService(launchIntent); - EventDistributor.getInstance().sendDownloadQueuedBroadcast(); - return true; - } - - private void download(Context context, FeedFile item, File dest, - boolean overwriteIfExists, String username, String password, boolean deleteOnFailure) { - if (!isDownloadingFile(item)) { - if (!isFilenameAvailable(dest.toString()) || (deleteOnFailure && dest.exists())) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Filename already used."); - if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { - boolean result = dest.delete(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting file. Result: " + result); - } else { - // find different name - File newDest = null; - for (int i = 1; i < Integer.MAX_VALUE; i++) { - String newName = FilenameUtils.getBaseName(dest - .getName()) - + "-" - + i - + FilenameUtils.EXTENSION_SEPARATOR - + FilenameUtils.getExtension(dest.getName()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Testing filename " + newName); - newDest = new File(dest.getParent(), newName); - if (!newDest.exists() - && isFilenameAvailable(newDest.toString())) { - if (BuildConfig.DEBUG) - Log.d(TAG, "File doesn't exist yet. Using " - + newName); - break; - } - } - if (newDest != null) { - dest = newDest; - } - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, - "Requesting download of url " + item.getDownload_url()); - item.setDownload_url(URLChecker.prepareURL(item.getDownload_url())); - - DownloadRequest request = new DownloadRequest(dest.toString(), - URLChecker.prepareURL(item.getDownload_url()), item.getHumanReadableIdentifier(), - item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure); - - download(context, request); - } else { - Log.e(TAG, "URL " + item.getDownload_url() - + " is already being downloaded"); - } - } - - /** - * Returns true if a filename is available and false if it has already been - * taken by another requested download. - */ - private boolean isFilenameAvailable(String path) { - for (String key : downloads.keySet()) { - DownloadRequest r = downloads.get(key); - if (StringUtils.equals(r.getDestination(), path)) { - if (BuildConfig.DEBUG) - Log.d(TAG, path - + " is already used by another requested download"); - return false; - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, path + " is available as a download destination"); - return true; - } - - public void downloadFeed(Context context, Feed feed) - throws DownloadRequestException { - if (feedFileValid(feed)) { - String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; - String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; - - download(context, feed, new File(getFeedfilePath(context), - getFeedfileName(feed)), true, username, password, true); - } - } - - public void downloadImage(Context context, FeedImage image) - throws DownloadRequestException { - if (feedFileValid(image)) { - download(context, image, new File(getImagefilePath(context), - getImagefileName(image)), false, null, null, false); - } - } - - public void downloadMedia(Context context, FeedMedia feedmedia) - throws DownloadRequestException { - if (feedFileValid(feedmedia)) { - Feed feed = feedmedia.getItem().getFeed(); - String username; - String password; - if (feed != null && feed.getPreferences() != null) { - username = feed.getPreferences().getUsername(); - password = feed.getPreferences().getPassword(); - } else { - username = null; - password = null; - } - - File dest; - if (feedmedia.getFile_url() != null) { - dest = new File(feedmedia.getFile_url()); - } else { - dest = new File(getMediafilePath(context, feedmedia), - getMediafilename(feedmedia)); - } - download(context, feedmedia, - dest, false, username, password, false - ); - } - } - - /** - * Throws a DownloadRequestException if the feedfile or the download url of - * the feedfile is null. - * - * @throws DownloadRequestException - */ - private boolean feedFileValid(FeedFile f) throws DownloadRequestException { - if (f == null) { - throw new DownloadRequestException("Feedfile was null"); - } else if (f.getDownload_url() == null) { - throw new DownloadRequestException("File has no download URL"); - } else { - return true; - } - } - - /** - * Cancels a running download. - */ - public void cancelDownload(final Context context, final FeedFile f) { - cancelDownload(context, f.getDownload_url()); - } - - /** - * Cancels a running download. - */ - public void cancelDownload(final Context context, final String downloadUrl) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelling download with url " + downloadUrl); - Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); - cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl); - context.sendBroadcast(cancelIntent); - } - - /** - * Cancels all running downloads - */ - public void cancelAllDownloads(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelling all running downloads"); - context.sendBroadcast(new Intent( - DownloadService.ACTION_CANCEL_ALL_DOWNLOADS)); - } - - /** - * Returns true if there is at least one Feed in the downloads queue. - */ - public boolean isDownloadingFeeds() { - for (DownloadRequest r : downloads.values()) { - if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - return true; - } - } - return false; - } - - /** - * Checks if feedfile is in the downloads list - */ - public boolean isDownloadingFile(FeedFile item) { - if (item.getDownload_url() != null) { - return downloads.containsKey(item.getDownload_url()); - } - return false; - } - - public DownloadRequest getDownload(String downloadUrl) { - return downloads.get(downloadUrl); - } - - /** - * Checks if feedfile with the given download url is in the downloads list - */ - public boolean isDownloadingFile(String downloadUrl) { - return downloads.get(downloadUrl) != null; - } - - public boolean hasNoDownloads() { - return downloads.isEmpty(); - } - - /** - * Remove an object from the downloads-list of the requester. - */ - public void removeDownload(DownloadRequest r) { - if (downloads.remove(r.getSource()) == null) { - Log.e(TAG, - "Could not remove object with url " + r.getSource()); - } - } - - /** - * Get the number of uncompleted Downloads - */ - public int getNumberOfDownloads() { - return downloads.size(); - } - - public String getFeedfilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) - .toString() + "/"; - } - - public String getFeedfileName(Feed feed) { - String filename = feed.getDownload_url(); - if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { - filename = feed.getTitle(); - } - return "feed-" + FileNameGenerator.generateFileName(filename); - } - - public String getImagefilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH) - .toString() + "/"; - } - - public String getImagefileName(FeedImage image) { - String filename = image.getDownload_url(); - if (image.getOwner() != null && image.getOwner().getHumanReadableIdentifier() != null) { - filename = image.getOwner().getHumanReadableIdentifier(); - } - return "image-" + FileNameGenerator.generateFileName(filename); - } - - public String getMediafilePath(Context context, FeedMedia media) - throws DownloadRequestException { - File externalStorage = getExternalFilesDirOrThrowException( - context, - MEDIA_DOWNLOADPATH - + FileNameGenerator.generateFileName(media.getItem() - .getFeed().getTitle()) + "/" - ); - return externalStorage.toString(); - } - - private File getExternalFilesDirOrThrowException(Context context, - String type) throws DownloadRequestException { - File result = UserPreferences.getDataFolder(context, type); - if (result == null) { - throw new DownloadRequestException( - "Failed to access external storage"); - } - return result; - } - - public String getMediafilename(FeedMedia media) { - String filename; - String titleBaseFilename = ""; - - // Try to generate the filename by the item title - if (media.getItem() != null && media.getItem().getTitle() != null) { - String title = media.getItem().getTitle(); - // Delete reserved characters - titleBaseFilename = title.replaceAll("[\\\\/%\\?\\*:|<>\"\\p{Cntrl}]", ""); - titleBaseFilename = titleBaseFilename.trim(); - } - - String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(), - null, media.getMime_type()); - ; - - if (titleBaseFilename != "") { - // Append extension - filename = titleBaseFilename + FilenameUtils.EXTENSION_SEPARATOR + - FilenameUtils.getExtension(URLBaseFilename); - } else { - // Fall back on URL file name - filename = URLBaseFilename; - } - return filename; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/FeedItemStatistics.java b/app/src/main/java/de/danoeh/antennapod/storage/FeedItemStatistics.java deleted file mode 100644 index 8cb040756..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/FeedItemStatistics.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.danoeh.antennapod.storage; - -import java.util.Date; - -/** - * Contains information about a feed's items. - */ -public class FeedItemStatistics { - private long feedID; - private int numberOfItems; - private int numberOfNewItems; - private int numberOfInProgressItems; - private Date lastUpdate; - private static final Date UNKNOWN_DATE = new Date(0); - - - /** - * Creates new FeedItemStatistics object. - * - * @param feedID ID of the feed. - * @param numberOfItems Number of items that this feed has. - * @param numberOfNewItems Number of unread items this feed has. - * @param numberOfInProgressItems Number of items that the user has started listening to. - * @param lastUpdate pubDate of the latest episode. A lastUpdate value of 0 will be interpreted as DATE_UNKOWN if - * numberOfItems is 0. - */ - public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) { - this.feedID = feedID; - this.numberOfItems = numberOfItems; - this.numberOfNewItems = numberOfNewItems; - this.numberOfInProgressItems = numberOfInProgressItems; - if (numberOfItems > 0) { - this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; - } else { - this.lastUpdate = UNKNOWN_DATE; - } - } - - public long getFeedID() { - return feedID; - } - - public int getNumberOfItems() { - return numberOfItems; - } - - public int getNumberOfNewItems() { - return numberOfNewItems; - } - - public int getNumberOfInProgressItems() { - return numberOfInProgressItems; - } - - /** - * Returns the pubDate of the latest item in the feed. Users of this method - * should check if this value is unkown or not by calling lastUpdateKnown() first. - */ - public Date getLastUpdate() { - return (lastUpdate != null) ? (Date) lastUpdate.clone() : null; - } - - /** - * Returns true if the lastUpdate value is known. The lastUpdate value is unkown if the - * feed has no items. - */ - public boolean lastUpdateKnown() { - return lastUpdate != UNKNOWN_DATE; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/FeedSearcher.java b/app/src/main/java/de/danoeh/antennapod/storage/FeedSearcher.java deleted file mode 100644 index e7aa93f83..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/FeedSearcher.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.danoeh.antennapod.storage; - -import android.content.Context; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.SearchResult; -import de.danoeh.antennapod.util.comparator.SearchResultValueComparator; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; - -/** - * Performs search on Feeds and FeedItems - */ -public class FeedSearcher { - private static final String TAG = "FeedSearcher"; - - - /** - * Performs a search in all feeds or one specific feed. - */ - public static List performSearch(final Context context, - final String query, final long selectedFeed) { - final int values[] = {0, 0, 1, 2}; - final String[] subtitles = {context.getString(R.string.found_in_shownotes_label), - context.getString(R.string.found_in_shownotes_label), - context.getString(R.string.found_in_chapters_label), - context.getString(R.string.found_in_title_label)}; - - List result = new ArrayList(); - - FutureTask>[] tasks = new FutureTask[4]; - (tasks[0] = DBTasks.searchFeedItemContentEncoded(context, selectedFeed, query)).run(); - (tasks[1] = DBTasks.searchFeedItemDescription(context, selectedFeed, query)).run(); - (tasks[2] = DBTasks.searchFeedItemChapters(context, selectedFeed, query)).run(); - (tasks[3] = DBTasks.searchFeedItemTitle(context, selectedFeed, query)).run(); - try { - for (int i = 0; i < tasks.length; i++) { - FutureTask task = tasks[i]; - List items = (List) task.get(); - for (FeedItem item : items) { - result.add(new SearchResult(item, values[i], subtitles[i])); - } - - } - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - Collections.sort(result, new SearchResultValueComparator()); - return result; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/storage/PodDBAdapter.java b/app/src/main/java/de/danoeh/antennapod/storage/PodDBAdapter.java deleted file mode 100644 index 2a3aafe8c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/storage/PodDBAdapter.java +++ /dev/null @@ -1,1391 +0,0 @@ -package de.danoeh.antennapod.storage; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.MergeCursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDatabase.CursorFactory; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; - -import org.apache.commons.lang3.Validate; - -import java.util.Arrays; -import java.util.List; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedComponent; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.FeedPreferences; -import de.danoeh.antennapod.service.download.DownloadStatus; -import de.danoeh.antennapod.util.flattr.FlattrStatus; - -// TODO Remove media column from feeditem table - -/** - * Implements methods for accessing the database - */ -public class PodDBAdapter { - private static final String TAG = "PodDBAdapter"; - private static final int DATABASE_VERSION = 12; - public static final String DATABASE_NAME = "Antennapod.db"; - - /** - * Maximum number of arguments for IN-operator. - */ - public static final int IN_OPERATOR_MAXIMUM = 800; - - /** - * Maximum number of entries per search request. - */ - public static final int SEARCH_LIMIT = 30; - - // ----------- Column indices - // ----------- General indices - public static final int KEY_ID_INDEX = 0; - public static final int KEY_TITLE_INDEX = 1; - public static final int KEY_FILE_URL_INDEX = 2; - public static final int KEY_DOWNLOAD_URL_INDEX = 3; - public static final int KEY_DOWNLOADED_INDEX = 4; - public static final int KEY_LINK_INDEX = 5; - public static final int KEY_DESCRIPTION_INDEX = 6; - public static final int KEY_PAYMENT_LINK_INDEX = 7; - // ----------- Feed indices - public static final int KEY_LAST_UPDATE_INDEX = 8; - public static final int KEY_LANGUAGE_INDEX = 9; - public static final int KEY_AUTHOR_INDEX = 10; - public static final int KEY_IMAGE_INDEX = 11; - public static final int KEY_TYPE_INDEX = 12; - public static final int KEY_FEED_IDENTIFIER_INDEX = 13; - public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14; - public static final int KEY_FEED_USERNAME_INDEX = 15; - public static final int KEY_FEED_PASSWORD_INDEX = 16; - // ----------- FeedItem indices - public static final int KEY_CONTENT_ENCODED_INDEX = 2; - public static final int KEY_PUBDATE_INDEX = 3; - public static final int KEY_READ_INDEX = 4; - public static final int KEY_MEDIA_INDEX = 8; - public static final int KEY_FEED_INDEX = 9; - public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; - public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; - public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12; - // ---------- FeedMedia indices - public static final int KEY_DURATION_INDEX = 1; - public static final int KEY_POSITION_INDEX = 5; - public static final int KEY_SIZE_INDEX = 6; - public static final int KEY_MIME_TYPE_INDEX = 7; - public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; - public static final int KEY_MEDIA_FEEDITEM_INDEX = 9; - public static final int KEY_PLAYED_DURATION_INDEX = 10; - // --------- Download log indices - public static final int KEY_FEEDFILE_INDEX = 1; - public static final int KEY_FEEDFILETYPE_INDEX = 2; - public static final int KEY_REASON_INDEX = 3; - public static final int KEY_SUCCESSFUL_INDEX = 4; - public static final int KEY_COMPLETION_DATE_INDEX = 5; - public static final int KEY_REASON_DETAILED_INDEX = 6; - public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7; - // --------- Queue indices - public static final int KEY_FEEDITEM_INDEX = 1; - public static final int KEY_QUEUE_FEED_INDEX = 2; - // --------- Chapters indices - public static final int KEY_CHAPTER_START_INDEX = 2; - public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3; - public static final int KEY_CHAPTER_LINK_INDEX = 4; - public static final int KEY_CHAPTER_TYPE_INDEX = 5; - - // Key-constants - public static final String KEY_ID = "id"; - public static final String KEY_TITLE = "title"; - public static final String KEY_NAME = "name"; - public static final String KEY_LINK = "link"; - public static final String KEY_DESCRIPTION = "description"; - public static final String KEY_FILE_URL = "file_url"; - public static final String KEY_DOWNLOAD_URL = "download_url"; - public static final String KEY_PUBDATE = "pubDate"; - public static final String KEY_READ = "read"; - public static final String KEY_DURATION = "duration"; - public static final String KEY_POSITION = "position"; - public static final String KEY_SIZE = "filesize"; - public static final String KEY_MIME_TYPE = "mime_type"; - public static final String KEY_IMAGE = "image"; - public static final String KEY_FEED = "feed"; - public static final String KEY_MEDIA = "media"; - public static final String KEY_DOWNLOADED = "downloaded"; - public static final String KEY_LASTUPDATE = "last_update"; - public static final String KEY_FEEDFILE = "feedfile"; - public static final String KEY_REASON = "reason"; - public static final String KEY_SUCCESSFUL = "successful"; - public static final String KEY_FEEDFILETYPE = "feedfile_type"; - public static final String KEY_COMPLETION_DATE = "completion_date"; - public static final String KEY_FEEDITEM = "feeditem"; - public static final String KEY_CONTENT_ENCODED = "content_encoded"; - public static final String KEY_PAYMENT_LINK = "payment_link"; - public static final String KEY_START = "start"; - public static final String KEY_LANGUAGE = "language"; - public static final String KEY_AUTHOR = "author"; - public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; - public static final String KEY_TYPE = "type"; - public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; - public static final String KEY_FLATTR_STATUS = "flattr_status"; - public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; - public static final String KEY_REASON_DETAILED = "reason_detailed"; - public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; - public static final String KEY_CHAPTER_TYPE = "type"; - public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; - public static final String KEY_AUTO_DOWNLOAD = "auto_download"; - public static final String KEY_PLAYED_DURATION = "played_duration"; - public static final String KEY_USERNAME = "username"; - public static final String KEY_PASSWORD = "password"; - - // Table names - public static final String TABLE_NAME_FEEDS = "Feeds"; - public static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; - public static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; - public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; - public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; - public static final String TABLE_NAME_QUEUE = "Queue"; - public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; - - // SQL Statements for creating new tables - private static final String TABLE_PRIMARY_KEY = KEY_ID - + " INTEGER PRIMARY KEY AUTOINCREMENT ,"; - - private static final String CREATE_TABLE_FEEDS = "CREATE TABLE " - + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE - + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," - + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT," - + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," - + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR - + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," - + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," - + KEY_FLATTR_STATUS + " INTEGER," - + KEY_USERNAME + " TEXT," - + KEY_PASSWORD + " TEXT)"; - - private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " - + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE - + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE - + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," - + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," - + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," - + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," - + KEY_FLATTR_STATUS + " INTEGER," - + KEY_IMAGE + " INTEGER)"; - - private static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE " - + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE - + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," - + KEY_DOWNLOADED + " INTEGER)"; - - private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " - + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION - + " INTEGER," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL - + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION - + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," - + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," - + KEY_FEEDITEM + " INTEGER," - + KEY_PLAYED_DURATION + " INTEGER)"; - - private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " - + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE - + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON - + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE - + " INTEGER," + KEY_REASON_DETAILED + " TEXT," - + KEY_DOWNLOADSTATUS_TITLE + " TEXT)"; - - private static final String CREATE_TABLE_QUEUE = "CREATE TABLE " - + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY," - + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)"; - - private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE " - + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE - + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER," - + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)"; - - private SQLiteDatabase db; - private final Context context; - private PodDBHelper helper; - - /** - * Select all columns from the feed-table - */ - private static final String[] FEED_SEL_STD = { - TABLE_NAME_FEEDS + "." + KEY_ID, - TABLE_NAME_FEEDS + "." + KEY_TITLE, - TABLE_NAME_FEEDS + "." + KEY_FILE_URL, - TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL, - TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED, - TABLE_NAME_FEEDS + "." + KEY_LINK, - TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION, - TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK, - TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE, - TABLE_NAME_FEEDS + "." + KEY_LANGUAGE, - TABLE_NAME_FEEDS + "." + KEY_AUTHOR, - TABLE_NAME_FEEDS + "." + KEY_IMAGE, - TABLE_NAME_FEEDS + "." + KEY_TYPE, - TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, - TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, - TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS, - TABLE_NAME_FEEDS + "." + KEY_USERNAME, - TABLE_NAME_FEEDS + "." + KEY_PASSWORD - }; - - // column indices for FEED_SEL_STD - public static final int IDX_FEED_SEL_STD_ID = 0; - public static final int IDX_FEED_SEL_STD_TITLE = 1; - public static final int IDX_FEED_SEL_STD_FILE_URL = 2; - public static final int IDX_FEED_SEL_STD_DOWNLOAD_URL = 3; - public static final int IDX_FEED_SEL_STD_DOWNLOADED = 4; - public static final int IDX_FEED_SEL_STD_LINK = 5; - public static final int IDX_FEED_SEL_STD_DESCRIPTION = 6; - public static final int IDX_FEED_SEL_STD_PAYMENT_LINK = 7; - public static final int IDX_FEED_SEL_STD_LASTUPDATE = 8; - public static final int IDX_FEED_SEL_STD_LANGUAGE = 9; - public static final int IDX_FEED_SEL_STD_AUTHOR = 10; - public static final int IDX_FEED_SEL_STD_IMAGE = 11; - public static final int IDX_FEED_SEL_STD_TYPE = 12; - public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13; - public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14; - public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15; - public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 16; - public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 17; - - - /** - * Select all columns from the feeditems-table except description and - * content-encoded. - */ - private static final String[] FEEDITEM_SEL_FI_SMALL = { - TABLE_NAME_FEED_ITEMS + "." + KEY_ID, - TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE, - TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE, - TABLE_NAME_FEED_ITEMS + "." + KEY_READ, - TABLE_NAME_FEED_ITEMS + "." + KEY_LINK, - TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA, - TABLE_NAME_FEED_ITEMS + "." + KEY_FEED, - TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, - TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER, - TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS, - TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE}; - - /** - * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries. - */ - private static final String SEL_FI_SMALL_STR; - - static { - String selFiSmall = Arrays.toString(FEEDITEM_SEL_FI_SMALL); - SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1); - } - - // column indices for FEEDITEM_SEL_FI_SMALL - - public static final int IDX_FI_SMALL_ID = 0; - public static final int IDX_FI_SMALL_TITLE = 1; - public static final int IDX_FI_SMALL_PUBDATE = 2; - public static final int IDX_FI_SMALL_READ = 3; - public static final int IDX_FI_SMALL_LINK = 4; - public static final int IDX_FI_SMALL_PAYMENT_LINK = 5; - public static final int IDX_FI_SMALL_MEDIA = 6; - public static final int IDX_FI_SMALL_FEED = 7; - public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8; - public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9; - public static final int IDX_FI_SMALL_FLATTR_STATUS = 10; - public static final int IDX_FI_SMALL_IMAGE = 11; - - /** - * Select id, description and content-encoded column from feeditems. - */ - private static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION, - KEY_CONTENT_ENCODED, KEY_FEED}; - - // column indices for SEL_FI_EXTRA - - public static final int IDX_FI_EXTRA_ID = 0; - public static final int IDX_FI_EXTRA_DESCRIPTION = 1; - public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2; - public static final int IDX_FI_EXTRA_FEED = 3; - - static PodDBHelper dbHelperSingleton; - - private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) { - if (dbHelperSingleton == null) { - dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null, DATABASE_VERSION); - } - return dbHelperSingleton; - } - - public PodDBAdapter(Context c) { - this.context = c; - helper = getDbHelperSingleton(c.getApplicationContext()); - } - - public PodDBAdapter open() { - if (db == null || !db.isOpen() || db.isReadOnly()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Opening DB"); - try { - db = helper.getWritableDatabase(); - } catch (SQLException ex) { - ex.printStackTrace(); - db = helper.getReadableDatabase(); - } - } - return this; - } - - public void close() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Closing DB"); - //db.close(); - } - - public static boolean deleteDatabase(Context context) { - Log.w(TAG, "Deleting database"); - dbHelperSingleton.close(); - dbHelperSingleton = null; - return context.deleteDatabase(DATABASE_NAME); - } - - /** - * Inserts or updates a feed entry - * - * @return the id of the entry - */ - public long setFeed(Feed feed) { - ContentValues values = new ContentValues(); - values.put(KEY_TITLE, feed.getTitle()); - values.put(KEY_LINK, feed.getLink()); - values.put(KEY_DESCRIPTION, feed.getDescription()); - values.put(KEY_PAYMENT_LINK, feed.getPaymentLink()); - values.put(KEY_AUTHOR, feed.getAuthor()); - values.put(KEY_LANGUAGE, feed.getLanguage()); - if (feed.getImage() != null) { - if (feed.getImage().getId() == 0) { - setImage(feed.getImage()); - } - values.put(KEY_IMAGE, feed.getImage().getId()); - } - - values.put(KEY_FILE_URL, feed.getFile_url()); - values.put(KEY_DOWNLOAD_URL, feed.getDownload_url()); - values.put(KEY_DOWNLOADED, feed.isDownloaded()); - values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime()); - values.put(KEY_TYPE, feed.getType()); - values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); - - Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong()); - - values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); - if (feed.getId() == 0) { - // Create new entry - if (BuildConfig.DEBUG) - Log.d(this.toString(), "Inserting new Feed into db"); - feed.setId(db.insert(TABLE_NAME_FEEDS, null, values)); - } else { - if (BuildConfig.DEBUG) - Log.d(this.toString(), "Updating existing Feed in db"); - db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", - new String[]{String.valueOf(feed.getId())}); - - } - return feed.getId(); - } - - public void setFeedPreferences(FeedPreferences prefs) { - if (prefs.getFeedID() == 0) { - throw new IllegalArgumentException("Feed ID of preference must not be null"); - } - ContentValues values = new ContentValues(); - values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload()); - values.put(KEY_USERNAME, prefs.getUsername()); - values.put(KEY_PASSWORD, prefs.getPassword()); - db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); - } - - /** - * Inserts or updates an image entry - * - * @return the id of the entry - */ - public long setImage(FeedImage image) { - db.beginTransaction(); - ContentValues values = new ContentValues(); - values.put(KEY_TITLE, image.getTitle()); - values.put(KEY_DOWNLOAD_URL, image.getDownload_url()); - values.put(KEY_DOWNLOADED, image.isDownloaded()); - values.put(KEY_FILE_URL, image.getFile_url()); - if (image.getId() == 0) { - image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values)); - } else { - db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?", - new String[]{String.valueOf(image.getId())}); - } - - final FeedComponent owner = image.getOwner(); - if (owner != null && owner.getId() != 0) { - values.clear(); - values.put(KEY_IMAGE, image.getId()); - if (owner instanceof Feed) { - db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())}); - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - return image.getId(); - } - - /** - * Inserts or updates an image entry - * - * @return the id of the entry - */ - public long setMedia(FeedMedia media) { - ContentValues values = new ContentValues(); - values.put(KEY_DURATION, media.getDuration()); - values.put(KEY_POSITION, media.getPosition()); - values.put(KEY_SIZE, media.getSize()); - values.put(KEY_MIME_TYPE, media.getMime_type()); - values.put(KEY_DOWNLOAD_URL, media.getDownload_url()); - values.put(KEY_DOWNLOADED, media.isDownloaded()); - values.put(KEY_FILE_URL, media.getFile_url()); - - if (media.getPlaybackCompletionDate() != null) { - values.put(KEY_PLAYBACK_COMPLETION_DATE, media - .getPlaybackCompletionDate().getTime()); - } else { - values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); - } - if (media.getItem() != null) { - values.put(KEY_FEEDITEM, media.getItem().getId()); - } - if (media.getId() == 0) { - media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values)); - } else { - db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", - new String[]{String.valueOf(media.getId())}); - } - return media.getId(); - } - - public void setFeedMediaPlaybackInformation(FeedMedia media) { - if (media.getId() != 0) { - ContentValues values = new ContentValues(); - values.put(KEY_POSITION, media.getPosition()); - values.put(KEY_DURATION, media.getDuration()); - values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); - db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", - new String[]{String.valueOf(media.getId())}); - } else { - Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0"); - } - } - - public void setFeedMediaPlaybackCompletionDate(FeedMedia media) { - if (media.getId() != 0) { - ContentValues values = new ContentValues(); - values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime()); - values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); - db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", - new String[]{String.valueOf(media.getId())}); - } else { - Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0"); - } - } - - /** - * Insert all FeedItems of a feed and the feed object itself in a single - * transaction - */ - public void setCompleteFeed(Feed... feeds) { - db.beginTransaction(); - for (Feed feed : feeds) { - setFeed(feed); - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - setFeedItem(item, false); - } - } - if (feed.getPreferences() != null) { - setFeedPreferences(feed.getPreferences()); - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - /** - * Update the flattr status of a feed - */ - public void setFeedFlattrStatus(Feed feed) { - ContentValues values = new ContentValues(); - values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); - db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); - } - - /** - * Get all feeds in the flattr queue. - */ - public Cursor getFeedsInFlattrQueueCursor() { - return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?", - new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); - } - - /** - * Get all feed items in the flattr queue. - */ - public Cursor getFeedItemsInFlattrQueueCursor() { - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?", - new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); - } - - /** - * Counts feeds and feed items in the flattr queue - */ - public int getFlattrQueueSize() { - int res = 0; - Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", - TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); - if (c.moveToFirst()) { - res = c.getInt(0); - c.close(); - } else { - Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds"); - } - c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", - TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); - if (c.moveToFirst()) { - res += c.getInt(0); - c.close(); - } else { - Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items"); - } - - return res; - } - - /** - * Updates the download URL of a Feed. - */ - public void setFeedDownloadUrl(String original, String updated) { - ContentValues values = new ContentValues(); - values.put(KEY_DOWNLOAD_URL, updated); - db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original}); - } - - public void setFeedItemlist(List items) { - db.beginTransaction(); - for (FeedItem item : items) { - setFeedItem(item, true); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public long setSingleFeedItem(FeedItem item) { - db.beginTransaction(); - long result = setFeedItem(item, true); - db.setTransactionSuccessful(); - db.endTransaction(); - return result; - } - - /** - * Update the flattr status of a FeedItem - */ - public void setFeedItemFlattrStatus(FeedItem feedItem) { - ContentValues values = new ContentValues(); - values.put(KEY_FLATTR_STATUS, feedItem.getFlattrStatus().toLong()); - db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(feedItem.getId())}); - } - - /** - * Update the flattr status of a feed or feed item specified by its payment link - * and the new flattr status to use - */ - public void setItemFlattrStatus(String url, FlattrStatus status) { - //Log.d(TAG, "setItemFlattrStatus(" + url + ") = " + status.toString()); - ContentValues values = new ContentValues(); - values.put(KEY_FLATTR_STATUS, status.toLong()); - - // regexps in sqlite would be neat! - String[] query_urls = new String[]{ - "*" + url + "&*", - "*" + url + "%2F&*", - "*" + url + "", - "*" + url + "%2F" - }; - - if (db.update(TABLE_NAME_FEEDS, values, - KEY_PAYMENT_LINK + " GLOB ?" - + " OR " + KEY_PAYMENT_LINK + " GLOB ?" - + " OR " + KEY_PAYMENT_LINK + " GLOB ?" - + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls - ) > 0) { - Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in Feeds table"); - return; - } - if (db.update(TABLE_NAME_FEED_ITEMS, values, - KEY_PAYMENT_LINK + " GLOB ?" - + " OR " + KEY_PAYMENT_LINK + " GLOB ?" - + " OR " + KEY_PAYMENT_LINK + " GLOB ?" - + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls - ) > 0) { - Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in FeedsItems table"); - } - } - - /** - * Reset flattr status to unflattrd for all items - */ - public void clearAllFlattrStatus() { - ContentValues values = new ContentValues(); - values.put(KEY_FLATTR_STATUS, 0); - db.update(TABLE_NAME_FEEDS, values, null, null); - db.update(TABLE_NAME_FEED_ITEMS, values, null, null); - } - - /** - * Inserts or updates a feeditem entry - * - * @param item The FeedItem - * @param saveFeed true if the Feed of the item should also be saved. This should be set to - * false if the method is executed on a list of FeedItems of the same Feed. - * @return the id of the entry - */ - private long setFeedItem(FeedItem item, boolean saveFeed) { - ContentValues values = new ContentValues(); - values.put(KEY_TITLE, item.getTitle()); - values.put(KEY_LINK, item.getLink()); - if (item.getDescription() != null) { - values.put(KEY_DESCRIPTION, item.getDescription()); - } - if (item.getContentEncoded() != null) { - values.put(KEY_CONTENT_ENCODED, item.getContentEncoded()); - } - values.put(KEY_PUBDATE, item.getPubDate().getTime()); - values.put(KEY_PAYMENT_LINK, item.getPaymentLink()); - if (saveFeed && item.getFeed() != null) { - setFeed(item.getFeed()); - } - values.put(KEY_FEED, item.getFeed().getId()); - values.put(KEY_READ, item.isRead()); - values.put(KEY_HAS_CHAPTERS, item.getChapters() != null); - values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); - values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong()); - if (item.hasItemImage()) { - if (item.getImage().getId() == 0) { - setImage(item.getImage()); - } - values.put(KEY_IMAGE, item.getImage().getId()); - } - - if (item.getId() == 0) { - item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); - } else { - db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", - new String[]{String.valueOf(item.getId())}); - } - if (item.getMedia() != null) { - setMedia(item.getMedia()); - } - if (item.getChapters() != null) { - setChapters(item); - } - return item.getId(); - } - - public void setFeedItemRead(boolean read, long itemId, long mediaId, - boolean resetMediaPosition) { - db.beginTransaction(); - ContentValues values = new ContentValues(); - - values.put(KEY_READ, read); - db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)}); - - if (resetMediaPosition) { - values.clear(); - values.put(KEY_POSITION, 0); - db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); - } - - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public void setFeedItemRead(boolean read, long... itemIds) { - db.beginTransaction(); - ContentValues values = new ContentValues(); - for (long id : itemIds) { - values.clear(); - values.put(KEY_READ, read); - db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(id)}); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public void setChapters(FeedItem item) { - ContentValues values = new ContentValues(); - for (Chapter chapter : item.getChapters()) { - values.put(KEY_TITLE, chapter.getTitle()); - values.put(KEY_START, chapter.getStart()); - values.put(KEY_FEEDITEM, item.getId()); - values.put(KEY_LINK, chapter.getLink()); - values.put(KEY_CHAPTER_TYPE, chapter.getChapterType()); - if (chapter.getId() == 0) { - chapter.setId(db - .insert(TABLE_NAME_SIMPLECHAPTERS, null, values)); - } else { - db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?", - new String[]{String.valueOf(chapter.getId())}); - } - } - } - - /** - * Inserts or updates a download status. - */ - public long setDownloadStatus(DownloadStatus status) { - ContentValues values = new ContentValues(); - values.put(KEY_FEEDFILE, status.getFeedfileId()); - values.put(KEY_FEEDFILETYPE, status.getFeedfileType()); - values.put(KEY_REASON, status.getReason().getCode()); - values.put(KEY_SUCCESSFUL, status.isSuccessful()); - values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime()); - values.put(KEY_REASON_DETAILED, status.getReasonDetailed()); - values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle()); - if (status.getId() == 0) { - status.setId(db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values)); - } else { - db.update(TABLE_NAME_DOWNLOAD_LOG, values, KEY_ID + "=?", - new String[]{String.valueOf(status.getId())}); - } - return status.getId(); - } - - public long getDownloadLogSize() { - final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_DOWNLOAD_LOG); - Cursor result = db.rawQuery(query, null); - long count = 0; - if (result.moveToFirst()) { - count = result.getLong(0); - } - result.close(); - return count; - } - - public void removeDownloadLogItems(long count) { - if (count > 0) { - final String sql = String.format("DELETE FROM %s WHERE %s in (SELECT %s from %s ORDER BY %s ASC LIMIT %d)", - TABLE_NAME_DOWNLOAD_LOG, KEY_ID, KEY_ID, TABLE_NAME_DOWNLOAD_LOG, KEY_COMPLETION_DATE, count); - db.execSQL(sql, null); - } - } - - public void setQueue(List queue) { - ContentValues values = new ContentValues(); - db.beginTransaction(); - db.delete(TABLE_NAME_QUEUE, null, null); - for (int i = 0; i < queue.size(); i++) { - FeedItem item = queue.get(i); - values.put(KEY_ID, i); - values.put(KEY_FEEDITEM, item.getId()); - values.put(KEY_FEED, item.getFeed().getId()); - db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values, - SQLiteDatabase.CONFLICT_REPLACE); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public void clearQueue() { - db.delete(TABLE_NAME_QUEUE, null, null); - } - - public void removeFeedMedia(FeedMedia media) { - db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?", - new String[]{String.valueOf(media.getId())}); - } - - public void removeChaptersOfItem(FeedItem item) { - db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?", - new String[]{String.valueOf(item.getId())}); - } - - public void removeFeedImage(FeedImage image) { - db.delete(TABLE_NAME_FEED_IMAGES, KEY_ID + "=?", - new String[]{String.valueOf(image.getId())}); - } - - /** - * Remove a FeedItem and its FeedMedia entry. - */ - public void removeFeedItem(FeedItem item) { - if (item.getMedia() != null) { - removeFeedMedia(item.getMedia()); - } - if (item.getChapters() != null) { - removeChaptersOfItem(item); - } - if (item.hasItemImage()) { - removeFeedImage(item.getImage()); - } - db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?", - new String[]{String.valueOf(item.getId())}); - } - - /** - * Remove a feed with all its FeedItems and Media entries. - */ - public void removeFeed(Feed feed) { - db.beginTransaction(); - if (feed.getImage() != null) { - removeFeedImage(feed.getImage()); - } - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - removeFeedItem(item); - } - } - - db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?", - new String[]{String.valueOf(feed.getId())}); - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public void removeDownloadStatus(DownloadStatus remove) { - db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?", - new String[]{String.valueOf(remove.getId())}); - } - - public void clearPlaybackHistory() { - ContentValues values = new ContentValues(); - values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); - db.update(TABLE_NAME_FEED_MEDIA, values, null, null); - } - - /** - * Get all Feeds from the Feed Table. - * - * @return The cursor of the query - */ - public final Cursor getAllFeedsCursor() { - Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null, - KEY_TITLE + " COLLATE NOCASE ASC"); - return c; - } - - public final Cursor getFeedCursorDownloadUrls() { - return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null); - } - - public final Cursor getExpiredFeedsCursor(long expirationTime) { - Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_LASTUPDATE + " < " + String.valueOf(System.currentTimeMillis() - expirationTime), - null, null, null, - null); - return c; - } - - /** - * Returns a cursor with all FeedItems of a Feed. Uses FEEDITEM_SEL_FI_SMALL - * - * @param feed The feed you want to get the FeedItems from. - * @return The cursor of the query - */ - public final Cursor getAllItemsOfFeedCursor(final Feed feed) { - return getAllItemsOfFeedCursor(feed.getId()); - } - - public final Cursor getAllItemsOfFeedCursor(final long feedId) { - Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED - + "=?", new String[]{String.valueOf(feedId)}, null, null, - null - ); - return c; - } - - /** - * Return a cursor with the SEL_FI_EXTRA selection of a single feeditem. - */ - public final Cursor getExtraInformationOfItem(final FeedItem item) { - Cursor c = db - .query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_ID + "=?", - new String[]{String.valueOf(item.getId())}, null, - null, null); - return c; - } - - /** - * Returns a cursor for a DB query in the FeedMedia table for a given ID. - * - * @param item The item you want to get the FeedMedia from - * @return The cursor of the query - */ - public final Cursor getFeedMediaOfItemCursor(final FeedItem item) { - Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", - new String[]{String.valueOf(item.getMedia().getId())}, null, - null, null); - return c; - } - - /** - * Returns a cursor for a DB query in the FeedImages table for a given ID. - * - * @param id ID of the FeedImage - * @return The cursor of the query - */ - public final Cursor getImageCursor(final long id) { - Cursor c = db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + "=?", - new String[]{String.valueOf(id)}, null, null, null); - return c; - } - - public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) { - Cursor c = db.query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_FEEDITEM - + "=?", new String[]{String.valueOf(item.getId())}, null, - null, null - ); - return c; - } - - public final Cursor getDownloadLogCursor(final int limit) { - Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null, - null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit); - return c; - } - - /** - * Returns a cursor which contains all feed items in the queue. The returned - * cursor uses the FEEDITEM_SEL_FI_SMALL selection. - */ - public final Cursor getQueueCursor() { - Object[] args = (Object[]) new String[]{ - SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID, - TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE, - TABLE_NAME_FEED_ITEMS + "." + KEY_ID, - TABLE_NAME_QUEUE + "." + KEY_FEEDITEM, - TABLE_NAME_QUEUE + "." + KEY_ID}; - String query = String.format( - "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args); - Cursor c = db.rawQuery(query, null); - /* - * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, - * "INNER JOIN ? ON ?=?", new String[] { TABLE_NAME_QUEUE, - * TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." + - * KEY_FEEDITEM }, null, null, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM); - */ - return c; - } - - public Cursor getQueueIDCursor() { - Cursor c = db.query(TABLE_NAME_QUEUE, new String[]{KEY_FEEDITEM}, null, null, null, null, KEY_ID + " ASC", null); - return c; - } - - /** - * Returns a cursor which contains all feed items in the unread items list. - * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. - */ - public final Cursor getUnreadItemsCursor() { - Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_READ - + "=0", null, null, null, KEY_PUBDATE + " DESC"); - return c; - } - - public final Cursor getUnreadItemIdsCursor() { - Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID}, - KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC"); - return c; - - } - - public final Cursor getRecentlyPublishedItemsCursor(int limit) { - Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, null, null, null, null, KEY_PUBDATE + " DESC LIMIT " + limit); - return c; - } - - public Cursor getDownloadedItemsCursor() { - final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS - + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE " - + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0"; - Cursor c = db.rawQuery(query, null); - return c; - } - - /** - * Returns a cursor which contains feed media objects with a playback - * completion date in ascending order. - * - * @param limit The maximum row count of the returned cursor. Must be an - * integer >= 0. - * @throws IllegalArgumentException if limit < 0 - */ - public final Cursor getCompletedMediaCursor(int limit) { - Validate.isTrue(limit >= 0, "Limit must be >= 0"); - - Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, - KEY_PLAYBACK_COMPLETION_DATE + " > 0 LIMIT " + limit, null, null, - null, null); - return c; - } - - public final Cursor getSingleFeedMediaCursor(long id) { - return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null); - } - - public final Cursor getFeedMediaCursorByItemID(String... mediaIds) { - int length = mediaIds.length; - if (length > IN_OPERATOR_MAXIMUM) { - Log.w(TAG, "Length of id array is larger than " - + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors"); - int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1; - Cursor[] cursors = new Cursor[numCursors]; - for (int i = 0; i < numCursors; i++) { - int neededLength = 0; - String[] parts = null; - final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM; - - if (elementsLeft >= IN_OPERATOR_MAXIMUM) { - neededLength = IN_OPERATOR_MAXIMUM; - parts = Arrays.copyOfRange(mediaIds, i - * IN_OPERATOR_MAXIMUM, (i + 1) - * IN_OPERATOR_MAXIMUM); - } else { - neededLength = elementsLeft; - parts = Arrays.copyOfRange(mediaIds, i - * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM) - + neededLength); - } - - cursors[i] = db.rawQuery("SELECT * FROM " - + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_FEEDITEM + " IN " - + buildInOperator(neededLength), parts); - } - return new MergeCursor(cursors); - } else { - return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN " - + buildInOperator(length), mediaIds, null, null, null); - } - } - - /** - * Builds an IN-operator argument depending on the number of items. - */ - private String buildInOperator(int size) { - if (size == 1) { - return "(?)"; - } - StringBuffer buffer = new StringBuffer("("); - for (int i = 0; i < size - 1; i++) { - buffer.append("?,"); - } - buffer.append("?)"); - return buffer.toString(); - } - - public final Cursor getFeedCursor(final long id) { - Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_ID + "=" + id, null, - null, null, null); - return c; - } - - public final Cursor getFeedItemCursor(final String... ids) { - if (ids.length > IN_OPERATOR_MAXIMUM) { - throw new IllegalArgumentException( - "number of IDs must not be larger than " - + IN_OPERATOR_MAXIMUM - ); - } - - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_ID + " IN " - + buildInOperator(ids.length), ids, null, null, null); - - } - - public int getQueueSize() { - final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE); - Cursor c = db.rawQuery(query, null); - int result = 0; - if (c.moveToFirst()) { - result = c.getInt(0); - } - c.close(); - return result; - } - - public final int getNumberOfUnreadItems() { - final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS + - " WHERE " + KEY_READ + " = 0"; - Cursor c = db.rawQuery(query, null); - int result = 0; - if (c.moveToFirst()) { - result = c.getInt(0); - } - c.close(); - return result; - } - - public final int getNumberOfDownloadedEpisodes() { - final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + - " WHERE " + KEY_DOWNLOADED + " > 0"; - - Cursor c = db.rawQuery(query, null); - int result = 0; - if (c.moveToFirst()) { - result = c.getInt(0); - } - c.close(); - return result; - } - - /** - * Uses DatabaseUtils to escape a search query and removes ' at the - * beginning and the end of the string returned by the escape method. - */ - private String prepareSearchQuery(String query) { - StringBuilder builder = new StringBuilder(); - DatabaseUtils.appendEscapedSQLString(builder, query); - builder.deleteCharAt(0); - builder.deleteCharAt(builder.length() - 1); - return builder.toString(); - } - - /** - * Searches for the given query in the description of all items or the items - * of a specified feed. - * - * @return A cursor with all search results in SEL_FI_EXTRA selection. - */ - public Cursor searchItemDescriptions(long feedID, String query) { - if (feedID != 0) { - // search items in specific feed - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED - + "=? AND " + KEY_DESCRIPTION + " LIKE '%" - + prepareSearchQuery(query) + "%'", - new String[]{String.valueOf(feedID)}, null, null, - null - ); - } else { - // search through all items - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, - KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query) - + "%'", null, null, null, null - ); - } - } - - /** - * Searches for the given query in the content-encoded field of all items or - * the items of a specified feed. - * - * @return A cursor with all search results in SEL_FI_EXTRA selection. - */ - public Cursor searchItemContentEncoded(long feedID, String query) { - if (feedID != 0) { - // search items in specific feed - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED - + "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%" - + prepareSearchQuery(query) + "%'", - new String[]{String.valueOf(feedID)}, null, null, - null - ); - } else { - // search through all items - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, - KEY_CONTENT_ENCODED + " LIKE '%" - + prepareSearchQuery(query) + "%'", null, null, - null, null - ); - } - } - - public Cursor searchItemTitles(long feedID, String query) { - if (feedID != 0) { - // search items in specific feed - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED - + "=? AND " + KEY_TITLE + " LIKE '%" - + prepareSearchQuery(query) + "%'", - new String[]{String.valueOf(feedID)}, null, null, - null - ); - } else { - // search through all items - return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, - KEY_TITLE + " LIKE '%" - + prepareSearchQuery(query) + "%'", null, null, - null, null - ); - } - } - - public Cursor searchItemChapters(long feedID, String searchQuery) { - final String query; - if (feedID != 0) { - query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + - TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + - TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + - feedID + " AND " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" - + prepareSearchQuery(searchQuery) + "%'"; - } else { - query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + - TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + - TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" - + prepareSearchQuery(searchQuery) + "%'"; - } - return db.rawQuery(query, null); - } - - - public static final int IDX_FEEDSTATISTICS_FEED = 0; - public static final int IDX_FEEDSTATISTICS_NUM_ITEMS = 1; - public static final int IDX_FEEDSTATISTICS_NEW_ITEMS = 2; - public static final int IDX_FEEDSTATISTICS_LATEST_EPISODE = 3; - public static final int IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES = 4; - - /** - * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result - * is sorted by the title of the feed. - */ - private static final String FEED_STATISTICS_QUERY = "SELECT Feeds.id, num_items, new_items, latest_episode, in_progress FROM " + - " Feeds LEFT JOIN " + - "(SELECT feed,count(*) AS num_items," + - " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," + - " MAX(pubDate) AS latest_episode," + - " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + - " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + - " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + - " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;"; - - public Cursor getFeedStatisticsCursor() { - return db.rawQuery(FEED_STATISTICS_QUERY, null); - } - - /** - * Helper class for opening the Antennapod database. - */ - private static class PodDBHelper extends SQLiteOpenHelper { - /** - * Constructor. - * - * @param context Context to use - * @param name Name of the database - * @param factory to use for creating cursor objects - * @param version number of the database - */ - public PodDBHelper(final Context context, final String name, - final CursorFactory factory, final int version) { - super(context, name, factory, version); - } - - @Override - public void onCreate(final SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_FEEDS); - db.execSQL(CREATE_TABLE_FEED_ITEMS); - db.execSQL(CREATE_TABLE_FEED_IMAGES); - db.execSQL(CREATE_TABLE_FEED_MEDIA); - db.execSQL(CREATE_TABLE_DOWNLOAD_LOG); - db.execSQL(CREATE_TABLE_QUEUE); - db.execSQL(CREATE_TABLE_SIMPLECHAPTERS); - } - - @Override - public void onUpgrade(final SQLiteDatabase db, final int oldVersion, - final int newVersion) { // TODO onUpgrade - Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " - + newVersion + "."); - if (oldVersion <= 1) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " - + KEY_TYPE + " TEXT"); - } - if (oldVersion <= 2) { - db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS - + " ADD COLUMN " + KEY_LINK + " TEXT"); - } - if (oldVersion <= 3) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_ITEM_IDENTIFIER + " TEXT"); - } - if (oldVersion <= 4) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " - + KEY_FEED_IDENTIFIER + " TEXT"); - } - if (oldVersion <= 5) { - db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG - + " ADD COLUMN " + KEY_REASON_DETAILED + " TEXT"); - db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG - + " ADD COLUMN " + KEY_DOWNLOADSTATUS_TITLE + " TEXT"); - } - if (oldVersion <= 6) { - db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS - + " ADD COLUMN " + KEY_CHAPTER_TYPE + " INTEGER"); - } - if (oldVersion <= 7) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_PLAYBACK_COMPLETION_DATE - + " INTEGER"); - } - if (oldVersion <= 8) { - final int KEY_ID_POSITION = 0; - final int KEY_MEDIA_POSITION = 1; - - // Add feeditem column to feedmedia table - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_FEEDITEM - + " INTEGER"); - Cursor feeditemCursor = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID, KEY_MEDIA}, "? > 0", new String[]{KEY_MEDIA}, null, null, null); - if (feeditemCursor.moveToFirst()) { - db.beginTransaction(); - ContentValues contentValues = new ContentValues(); - do { - long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); - contentValues.put(KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); - db.update(TABLE_NAME_FEED_MEDIA, contentValues, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); - contentValues.clear(); - } while (feeditemCursor.moveToNext()); - db.setTransactionSuccessful(); - db.endTransaction(); - } - feeditemCursor.close(); - } - if (oldVersion <= 9) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_AUTO_DOWNLOAD - + " INTEGER DEFAULT 1"); - } - if (oldVersion <= 10) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_FLATTR_STATUS - + " INTEGER"); - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_FLATTR_STATUS - + " INTEGER"); - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_PLAYED_DURATION - + " INTEGER"); - } - if (oldVersion <= 11) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_USERNAME - + " TEXT"); - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS - + " ADD COLUMN " + KEY_PASSWORD - + " TEXT"); - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_IMAGE - + " INTEGER"); - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandler.java b/app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandler.java deleted file mode 100644 index aafa1c209..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.danoeh.antennapod.syndication.handler; - -import de.danoeh.antennapod.feed.Feed; -import org.apache.commons.io.input.XmlStreamReader; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; -import java.io.File; -import java.io.IOException; -import java.io.Reader; - -public class FeedHandler { - - public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException, - ParserConfigurationException, UnsupportedFeedtypeException { - TypeGetter tg = new TypeGetter(); - TypeGetter.Type type = tg.getType(feed); - SyndHandler handler = new SyndHandler(feed, type); - - SAXParserFactory factory = SAXParserFactory.newInstance(); - factory.setNamespaceAware(true); - SAXParser saxParser = factory.newSAXParser(); - File file = new File(feed.getFile_url()); - Reader inputStreamReader = new XmlStreamReader(file); - InputSource inputSource = new InputSource(inputStreamReader); - - saxParser.parse(inputSource, handler); - inputStreamReader.close(); - return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java b/app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java deleted file mode 100644 index 41aa29b52..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/handler/FeedHandlerResult.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.syndication.handler; - -import de.danoeh.antennapod.feed.Feed; - -import java.util.Map; - -/** - * Container for results returned by the Feed parser - */ -public class FeedHandlerResult { - - public Feed feed; - public Map alternateFeedUrls; - - public FeedHandlerResult(Feed feed, Map alternateFeedUrls) { - this.feed = feed; - this.alternateFeedUrls = alternateFeedUrls; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/handler/HandlerState.java b/app/src/main/java/de/danoeh/antennapod/syndication/handler/HandlerState.java deleted file mode 100644 index 17f84724f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/handler/HandlerState.java +++ /dev/null @@ -1,98 +0,0 @@ -package de.danoeh.antennapod.syndication.handler; - -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.syndication.namespace.Namespace; -import de.danoeh.antennapod.syndication.namespace.SyndElement; - -import java.util.*; - -/** - * Contains all relevant information to describe the current state of a - * SyndHandler. - */ -public class HandlerState { - - /** - * Feed that the Handler is currently processing. - */ - protected Feed feed; - /** - * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the - * URL of the feed, the value is the title - */ - protected Map alternateUrls; - protected ArrayList items; - protected FeedItem currentItem; - protected Stack tagstack; - /** - * Namespaces that have been defined so far. - */ - protected HashMap namespaces; - protected Stack defaultNamespaces; - /** - * Buffer for saving characters. - */ - protected StringBuffer contentBuf; - - public HandlerState(Feed feed) { - this.feed = feed; - alternateUrls = new LinkedHashMap(); - items = new ArrayList(); - tagstack = new Stack(); - namespaces = new HashMap(); - defaultNamespaces = new Stack(); - } - - public Feed getFeed() { - return feed; - } - - public ArrayList getItems() { - return items; - } - - public FeedItem getCurrentItem() { - return currentItem; - } - - public Stack getTagstack() { - return tagstack; - } - - public void setFeed(Feed feed) { - this.feed = feed; - } - - public void setCurrentItem(FeedItem currentItem) { - this.currentItem = currentItem; - } - - /** - * Returns the SyndElement that comes after the top element of the tagstack. - */ - public SyndElement getSecondTag() { - SyndElement top = tagstack.pop(); - SyndElement second = tagstack.peek(); - tagstack.push(top); - return second; - } - - public SyndElement getThirdTag() { - SyndElement top = tagstack.pop(); - SyndElement second = tagstack.pop(); - SyndElement third = tagstack.peek(); - tagstack.push(second); - tagstack.push(top); - return third; - } - - public StringBuffer getContentBuf() { - return contentBuf; - } - - public void addAlternateFeedUrl(String title, String url) { - alternateUrls.put(url, title); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/handler/SyndHandler.java b/app/src/main/java/de/danoeh/antennapod/syndication/handler/SyndHandler.java deleted file mode 100644 index 15dc94d65..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/handler/SyndHandler.java +++ /dev/null @@ -1,126 +0,0 @@ -package de.danoeh.antennapod.syndication.handler; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.syndication.namespace.*; -import de.danoeh.antennapod.syndication.namespace.atom.NSAtom; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -/** Superclass for all SAX Handlers which process Syndication formats */ -public class SyndHandler extends DefaultHandler { - private static final String TAG = "SyndHandler"; - private static final String DEFAULT_PREFIX = ""; - protected HandlerState state; - - public SyndHandler(Feed feed, TypeGetter.Type type) { - state = new HandlerState(feed); - if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) { - state.defaultNamespaces.push(new NSRSS20()); - } - } - - @Override - public void startElement(String uri, String localName, String qName, - Attributes attributes) throws SAXException { - state.contentBuf = new StringBuffer(); - Namespace handler = getHandlingNamespace(uri, qName); - if (handler != null) { - SyndElement element = handler.handleElementStart(localName, state, - attributes); - state.tagstack.push(element); - - } - } - - @Override - public void characters(char[] ch, int start, int length) - throws SAXException { - if (!state.tagstack.empty()) { - if (state.getTagstack().size() >= 2) { - if (state.contentBuf != null) { - state.contentBuf.append(ch, start, length); - } - } - } - } - - @Override - public void endElement(String uri, String localName, String qName) - throws SAXException { - Namespace handler = getHandlingNamespace(uri, qName); - if (handler != null) { - handler.handleElementEnd(localName, state); - state.tagstack.pop(); - - } - state.contentBuf = null; - - } - - @Override - public void endPrefixMapping(String prefix) throws SAXException { - if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) { - state.defaultNamespaces.pop(); - } - } - - @Override - public void startPrefixMapping(String prefix, String uri) - throws SAXException { - // Find the right namespace - if (!state.namespaces.containsKey(uri)) { - if (uri.equals(NSAtom.NSURI)) { - if (prefix.equals(DEFAULT_PREFIX)) { - state.defaultNamespaces.push(new NSAtom()); - } else if (prefix.equals(NSAtom.NSTAG)) { - state.namespaces.put(uri, new NSAtom()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized Atom namespace"); - } - } else if (uri.equals(NSContent.NSURI) - && prefix.equals(NSContent.NSTAG)) { - state.namespaces.put(uri, new NSContent()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized Content namespace"); - } else if (uri.equals(NSITunes.NSURI) - && prefix.equals(NSITunes.NSTAG)) { - state.namespaces.put(uri, new NSITunes()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized ITunes namespace"); - } else if (uri.equals(NSSimpleChapters.NSURI) - && prefix.matches(NSSimpleChapters.NSTAG)) { - state.namespaces.put(uri, new NSSimpleChapters()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized SimpleChapters namespace"); - } else if (uri.equals(NSMedia.NSURI) - && prefix.equals(NSMedia.NSTAG)) { - state.namespaces.put(uri, new NSMedia()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized media namespace"); - } - } - } - - private Namespace getHandlingNamespace(String uri, String qName) { - Namespace handler = state.namespaces.get(uri); - if (handler == null && !state.defaultNamespaces.empty() - && !qName.contains(":")) { - handler = state.defaultNamespaces.peek(); - } - return handler; - } - - @Override - public void endDocument() throws SAXException { - super.endDocument(); - state.getFeed().setItems(state.getItems()); - } - - public HandlerState getState() { - return state; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/handler/TypeGetter.java b/app/src/main/java/de/danoeh/antennapod/syndication/handler/TypeGetter.java deleted file mode 100644 index 2496e112a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/handler/TypeGetter.java +++ /dev/null @@ -1,112 +0,0 @@ -package de.danoeh.antennapod.syndication.handler; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Feed; -import org.apache.commons.io.input.XmlStreamReader; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Reader; - -/** Gets the type of a specific feed by reading the root element. */ -public class TypeGetter { - private static final String TAG = "TypeGetter"; - - public enum Type { - RSS20, RSS091, ATOM, INVALID - } - - private static final String ATOM_ROOT = "feed"; - private static final String RSS_ROOT = "rss"; - - public Type getType(Feed feed) throws UnsupportedFeedtypeException { - XmlPullParserFactory factory; - if (feed.getFile_url() != null) { - try { - factory = XmlPullParserFactory.newInstance(); - factory.setNamespaceAware(true); - XmlPullParser xpp = factory.newPullParser(); - xpp.setInput(createReader(feed)); - int eventType = xpp.getEventType(); - - while (eventType != XmlPullParser.END_DOCUMENT) { - if (eventType == XmlPullParser.START_TAG) { - String tag = xpp.getName(); - if (tag.equals(ATOM_ROOT)) { - feed.setType(Feed.TYPE_ATOM1); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized type Atom"); - return Type.ATOM; - } else if (tag.equals(RSS_ROOT)) { - String strVersion = xpp.getAttributeValue(null, - "version"); - if (strVersion != null) { - - if (strVersion.equals("2.0")) { - feed.setType(Feed.TYPE_RSS2); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized type RSS 2.0"); - return Type.RSS20; - } else if (strVersion.equals("0.91") - || strVersion.equals("0.92")) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Recognized type RSS 0.91/0.92"); - return Type.RSS091; - } - } - throw new UnsupportedFeedtypeException(Type.INVALID); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Type is invalid"); - throw new UnsupportedFeedtypeException(Type.INVALID, tag); - } - } else { - eventType = xpp.next(); - } - } - - } catch (XmlPullParserException e) { - e.printStackTrace(); - // XML document might actually be a HTML document -> try to parse as HTML - String rootElement = null; - try { - if (Jsoup.parse(new File(feed.getFile_url()), null) != null) { - rootElement = "html"; - } - } catch (IOException e1) { - e1.printStackTrace(); - } finally { - throw new UnsupportedFeedtypeException(Type.INVALID, rootElement); - } - - } catch (IOException e) { - e.printStackTrace(); - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Type is invalid"); - throw new UnsupportedFeedtypeException(Type.INVALID); - } - - private Reader createReader(Feed feed) { - Reader reader; - try { - reader = new XmlStreamReader(new File(feed.getFile_url())); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } catch (IOException e) { - e.printStackTrace(); - return null; - } - return reader; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java b/app/src/main/java/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java deleted file mode 100644 index 605dad2fb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.danoeh.antennapod.syndication.handler; - -import de.danoeh.antennapod.syndication.handler.TypeGetter.Type; - -public class UnsupportedFeedtypeException extends Exception { - private static final long serialVersionUID = 9105878964928170669L; - private TypeGetter.Type type; - private String rootElement; - - public UnsupportedFeedtypeException(Type type) { - super(); - this.type = type; - } - - public UnsupportedFeedtypeException(Type type, String rootElement) { - this.type = type; - this.rootElement = rootElement; - } - - public TypeGetter.Type getType() { - return type; - } - - public String getRootElement() { - return rootElement; - } - - @Override - public String getMessage() { - if (type == TypeGetter.Type.INVALID) { - return "Invalid type"; - } else { - return "Type " + type + " not supported"; - } - } - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSContent.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSContent.java deleted file mode 100644 index 9ad3026be..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSContent.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace; - -import de.danoeh.antennapod.syndication.handler.HandlerState; -import org.xml.sax.Attributes; - -public class NSContent extends Namespace { - public static final String NSTAG = "content"; - public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; - - private static final String ENCODED = "encoded"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (localName.equals(ENCODED)) { - state.getCurrentItem().setContentEncoded(state.getContentBuf().toString()); - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSITunes.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSITunes.java deleted file mode 100644 index d8cbe040b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSITunes.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace; - -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.syndication.handler.HandlerState; -import org.xml.sax.Attributes; - -public class NSITunes extends Namespace { - public static final String NSTAG = "itunes"; - public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd"; - - private static final String IMAGE = "image"; - private static final String IMAGE_TITLE = "image"; - private static final String IMAGE_HREF = "href"; - - private static final String AUTHOR = "author"; - - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (localName.equals(IMAGE)) { - FeedImage image = new FeedImage(); - image.setTitle(IMAGE_TITLE); - image.setDownload_url(attributes.getValue(IMAGE_HREF)); - - if (state.getCurrentItem() != null) { - // this is an items image - image.setTitle(state.getCurrentItem().getTitle() + IMAGE_TITLE); - state.getCurrentItem().setImage(image); - - } else { - // this is the feed image - if (state.getFeed().getImage() == null) { - state.getFeed().setImage(image); - } - } - - } - - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (localName.equals(AUTHOR)) { - state.getFeed().setAuthor(state.getContentBuf().toString()); - } - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSMedia.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSMedia.java deleted file mode 100644 index cc23167c1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSMedia.java +++ /dev/null @@ -1,68 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.syndication.handler.HandlerState; -import de.danoeh.antennapod.syndication.util.SyndTypeUtils; -import org.xml.sax.Attributes; - -import java.util.concurrent.TimeUnit; - -/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ -public class NSMedia extends Namespace { - private static final String TAG = "NSMedia"; - - public static final String NSTAG = "media"; - public static final String NSURI = "http://search.yahoo.com/mrss/"; - - private static final String CONTENT = "content"; - private static final String DOWNLOAD_URL = "url"; - private static final String SIZE = "fileSize"; - private static final String MIME_TYPE = "type"; - private static final String DURATION = "duration"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (localName.equals(CONTENT)) { - String url = attributes.getValue(DOWNLOAD_URL); - String type = attributes.getValue(MIME_TYPE); - if (state.getCurrentItem().getMedia() == null - && url != null - && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils - .getValidMimeTypeFromUrl(url)) != null))) { - - long size = 0; - try { - size = Long.parseLong(attributes.getValue(SIZE)); - } catch (NumberFormatException e) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Length attribute could not be parsed."); - } - - int duration = 0; - try { - String durationStr = attributes.getValue(DURATION); - if (durationStr != null) { - duration = (int) TimeUnit.MILLISECONDS.convert( - Long.parseLong(durationStr), TimeUnit.SECONDS); - } - } catch (NumberFormatException e) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Duration attribute could not be parsed"); - } - - state.getCurrentItem().setMedia( - new FeedMedia(state.getCurrentItem(), url, size, type)); - } - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSRSS20.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSRSS20.java deleted file mode 100644 index 9572f87ae..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSRSS20.java +++ /dev/null @@ -1,141 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.syndication.handler.HandlerState; -import de.danoeh.antennapod.syndication.util.SyndDateUtils; -import de.danoeh.antennapod.syndication.util.SyndTypeUtils; -import org.xml.sax.Attributes; - -/** - * SAX-Parser for reading RSS-Feeds - * - * @author daniel - * - */ -public class NSRSS20 extends Namespace { - private static final String TAG = "NSRSS20"; - public static final String NSTAG = "rss"; - public static final String NSURI = ""; - - public final static String CHANNEL = "channel"; - public final static String ITEM = "item"; - public final static String GUID = "guid"; - public final static String TITLE = "title"; - public final static String LINK = "link"; - public final static String DESCR = "description"; - public final static String PUBDATE = "pubDate"; - public final static String ENCLOSURE = "enclosure"; - public final static String IMAGE = "image"; - public final static String URL = "url"; - public final static String LANGUAGE = "language"; - - public final static String ENC_URL = "url"; - public final static String ENC_LEN = "length"; - public final static String ENC_TYPE = "type"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (localName.equals(ITEM)) { - state.setCurrentItem(new FeedItem()); - state.getItems().add(state.getCurrentItem()); - state.getCurrentItem().setFeed(state.getFeed()); - - } else if (localName.equals(ENCLOSURE)) { - String type = attributes.getValue(ENC_TYPE); - String url = attributes.getValue(ENC_URL); - if (state.getCurrentItem().getMedia() == null - && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils - .getValidMimeTypeFromUrl(url)) != null))) { - - long size = 0; - try { - size = Long.parseLong(attributes.getValue(ENC_LEN)); - } catch (NumberFormatException e) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Length attribute could not be parsed."); - } - state.getCurrentItem().setMedia( - new FeedMedia(state.getCurrentItem(), url, size, type)); - } - - } else if (localName.equals(IMAGE)) { - if (state.getTagstack().size() >= 1) { - String parent = state.getTagstack().peek().getName(); - if (parent.equals(CHANNEL)) { - state.getFeed().setImage(new FeedImage()); - } - } - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (localName.equals(ITEM)) { - if (state.getCurrentItem() != null) { - // the title tag is optional in RSS 2.0. The description is used - // as a - // title if the item has no title-tag. - if (state.getCurrentItem().getTitle() == null) { - state.getCurrentItem().setTitle( - state.getCurrentItem().getDescription()); - } - } - state.setCurrentItem(null); - } else if (state.getTagstack().size() >= 2 - && state.getContentBuf() != null) { - String content = state.getContentBuf().toString(); - SyndElement topElement = state.getTagstack().peek(); - String top = topElement.getName(); - SyndElement secondElement = state.getSecondTag(); - String second = secondElement.getName(); - String third = null; - if (state.getTagstack().size() >= 3) { - third = state.getThirdTag().getName(); - } - - if (top.equals(GUID) && second.equals(ITEM)) { - // some feed creators include an empty or non-standard guid-element in their feed, which should be ignored - if (!content.isEmpty()) { - state.getCurrentItem().setItemIdentifier(content); - } - } else if (top.equals(TITLE)) { - if (second.equals(ITEM)) { - state.getCurrentItem().setTitle(content); - } else if (second.equals(CHANNEL)) { - state.getFeed().setTitle(content); - } else if (second.equals(IMAGE) && third != null - && third.equals(CHANNEL)) { - state.getFeed().getImage().setTitle(content); - } - } else if (top.equals(LINK)) { - if (second.equals(CHANNEL)) { - state.getFeed().setLink(content); - } else if (second.equals(ITEM)) { - state.getCurrentItem().setLink(content); - } - } else if (top.equals(PUBDATE) && second.equals(ITEM)) { - state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC822Date(content)); - } else if (top.equals(URL) && second.equals(IMAGE) && third != null - && third.equals(CHANNEL)) { - state.getFeed().getImage().setDownload_url(content); - } else if (localName.equals(DESCR)) { - if (second.equals(CHANNEL)) { - state.getFeed().setDescription(content); - } else if (second.equals(ITEM)) { - state.getCurrentItem().setDescription(content); - } - - } else if (localName.equals(LANGUAGE)) { - state.getFeed().setLanguage(content.toLowerCase()); - } - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java deleted file mode 100644 index 3f983ee88..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/NSSimpleChapters.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace; - -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.SimpleChapter; -import de.danoeh.antennapod.syndication.handler.HandlerState; -import de.danoeh.antennapod.syndication.util.SyndDateUtils; -import org.xml.sax.Attributes; - -import java.util.ArrayList; - -public class NSSimpleChapters extends Namespace { - public static final String NSTAG = "psc|sc"; - public static final String NSURI = "http://podlove.org/simple-chapters"; - - public static final String CHAPTERS = "chapters"; - public static final String CHAPTER = "chapter"; - public static final String START = "start"; - public static final String TITLE = "title"; - public static final String HREF = "href"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (localName.equals(CHAPTERS)) { - state.getCurrentItem().setChapters(new ArrayList()); - } else if (localName.equals(CHAPTER)) { - state.getCurrentItem() - .getChapters() - .add(new SimpleChapter(SyndDateUtils - .parseTimeString(attributes.getValue(START)), - attributes.getValue(TITLE), state.getCurrentItem(), - attributes.getValue(HREF))); - } - - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/Namespace.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/Namespace.java deleted file mode 100644 index 910131feb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/Namespace.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace; - -import de.danoeh.antennapod.syndication.handler.HandlerState; -import org.xml.sax.Attributes; - - -public abstract class Namespace { - public static final String NSTAG = null; - public static final String NSURI = null; - - /** Called by a Feedhandler when in startElement and it detects a namespace element - * @return The SyndElement to push onto the stack - * */ - public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes); - - /** Called by a Feedhandler when in endElement and it detects a namespace element - * @return true if namespace handled the element, false if it ignored it - * */ - public abstract void handleElementEnd(String localName, HandlerState state); - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/SyndElement.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/SyndElement.java deleted file mode 100644 index 187312c9e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/SyndElement.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace; - -/** Defines a XML Element that is pushed on the tagstack */ -public class SyndElement { - protected String name; - protected Namespace namespace; - - public SyndElement(String name, Namespace namespace) { - this.name = name; - this.namespace = namespace; - } - - public Namespace getNamespace() { - return namespace; - } - - public String getName() { - return name; - } - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java deleted file mode 100644 index 86b80d2ed..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace.atom; - -import de.danoeh.antennapod.syndication.namespace.Namespace; -import de.danoeh.antennapod.syndication.namespace.SyndElement; -import org.apache.commons.lang3.StringEscapeUtils; - -/** Represents Atom Element which contains text (content, title, summary). */ -public class AtomText extends SyndElement { - public static final String TYPE_TEXT = "text"; - public static final String TYPE_HTML = "html"; - public static final String TYPE_XHTML = "xhtml"; - - private String type; - private String content; - - public AtomText(String name, Namespace namespace, String type) { - super(name, namespace); - this.type = type; - } - - /** Processes the content according to the type and returns it. */ - public String getProcessedContent() { - if (type == null) { - return content; - } else if (type.equals(TYPE_HTML)) { - return StringEscapeUtils.unescapeHtml4(content); - } else if (type.equals(TYPE_XHTML)) { - return content; - } else { // Handle as text by default - return content; - } - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public String getType() { - return type; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java b/app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java deleted file mode 100644 index 2c8e232ff..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java +++ /dev/null @@ -1,194 +0,0 @@ -package de.danoeh.antennapod.syndication.namespace.atom; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.syndication.handler.HandlerState; -import de.danoeh.antennapod.syndication.namespace.NSRSS20; -import de.danoeh.antennapod.syndication.namespace.Namespace; -import de.danoeh.antennapod.syndication.namespace.SyndElement; -import de.danoeh.antennapod.syndication.util.SyndDateUtils; -import de.danoeh.antennapod.syndication.util.SyndTypeUtils; -import org.xml.sax.Attributes; - -public class NSAtom extends Namespace { - private static final String TAG = "NSAtom"; - public static final String NSTAG = "atom"; - public static final String NSURI = "http://www.w3.org/2005/Atom"; - - private static final String FEED = "feed"; - private static final String ID = "id"; - private static final String TITLE = "title"; - private static final String ENTRY = "entry"; - private static final String LINK = "link"; - private static final String UPDATED = "updated"; - private static final String AUTHOR = "author"; - private static final String CONTENT = "content"; - private static final String IMAGE = "logo"; - private static final String SUBTITLE = "subtitle"; - private static final String PUBLISHED = "published"; - - private static final String TEXT_TYPE = "type"; - // Link - private static final String LINK_HREF = "href"; - private static final String LINK_REL = "rel"; - private static final String LINK_TYPE = "type"; - private static final String LINK_TITLE = "title"; - private static final String LINK_LENGTH = "length"; - // rel-values - private static final String LINK_REL_ALTERNATE = "alternate"; - private static final String LINK_REL_ENCLOSURE = "enclosure"; - private static final String LINK_REL_PAYMENT = "payment"; - private static final String LINK_REL_RELATED = "related"; - private static final String LINK_REL_SELF = "self"; - // type-values - private static final String LINK_TYPE_ATOM = "application/atom+xml"; - private static final String LINK_TYPE_HTML = "text/html"; - private static final String LINK_TYPE_XHTML = "application/xml+xhtml"; - - private static final String LINK_TYPE_RSS = "application/rss+xml"; - - /** - * Regexp to test whether an Element is a Text Element. - */ - private static final String isText = TITLE + "|" + CONTENT + "|" + "|" - + SUBTITLE; - - public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL; - public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (localName.equals(ENTRY)) { - state.setCurrentItem(new FeedItem()); - state.getItems().add(state.getCurrentItem()); - state.getCurrentItem().setFeed(state.getFeed()); - } else if (localName.matches(isText)) { - String type = attributes.getValue(TEXT_TYPE); - return new AtomText(localName, this, type); - } else if (localName.equals(LINK)) { - String href = attributes.getValue(LINK_HREF); - String rel = attributes.getValue(LINK_REL); - SyndElement parent = state.getTagstack().peek(); - if (parent.getName().matches(isFeedItem)) { - if (rel == null || rel.equals(LINK_REL_ALTERNATE)) { - state.getCurrentItem().setLink(href); - } else if (rel.equals(LINK_REL_ENCLOSURE)) { - String strSize = attributes.getValue(LINK_LENGTH); - long size = 0; - try { - if (strSize != null) { - size = Long.parseLong(strSize); - } - } catch (NumberFormatException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "Length attribute could not be parsed."); - } - String type = attributes.getValue(LINK_TYPE); - if (SyndTypeUtils.enclosureTypeValid(type) - || (type = SyndTypeUtils - .getValidMimeTypeFromUrl(href)) != null) { - state.getCurrentItem().setMedia( - new FeedMedia(state.getCurrentItem(), href, - size, type) - ); - } - } else if (rel.equals(LINK_REL_PAYMENT)) { - state.getCurrentItem().setPaymentLink(href); - } - } else if (parent.getName().matches(isFeed)) { - if (rel == null || rel.equals(LINK_REL_ALTERNATE)) { - String type = attributes.getValue(LINK_TYPE); - /* - * Use as link if a) no type-attribute is given and - * feed-object has no link yet b) type of link is - * LINK_TYPE_HTML or LINK_TYPE_XHTML - */ - if ((type == null && state.getFeed().getLink() == null) - || (type != null && (type.equals(LINK_TYPE_HTML) || type.equals(LINK_TYPE_XHTML)))) { - state.getFeed().setLink(href); - } else if (type != null && (type.equals(LINK_TYPE_ATOM) || type.equals(LINK_TYPE_RSS))) { - // treat as podlove alternate feed - String title = attributes.getValue(LINK_TITLE); - if (title == null) { - title = href; - } - state.addAlternateFeedUrl(title, href); - } - } else if (rel.equals(LINK_REL_PAYMENT)) { - state.getFeed().setPaymentLink(href); - } - } - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (localName.equals(ENTRY)) { - state.setCurrentItem(null); - } - - if (state.getTagstack().size() >= 2) { - AtomText textElement = null; - String content; - if (state.getContentBuf() != null) { - content = state.getContentBuf().toString(); - } else { - content = ""; - } - SyndElement topElement = state.getTagstack().peek(); - String top = topElement.getName(); - SyndElement secondElement = state.getSecondTag(); - String second = secondElement.getName(); - - if (top.matches(isText)) { - textElement = (AtomText) topElement; - textElement.setContent(content); - } - - if (top.equals(ID)) { - if (second.equals(FEED)) { - state.getFeed().setFeedIdentifier(content); - } else if (second.equals(ENTRY)) { - state.getCurrentItem().setItemIdentifier(content); - } - } else if (top.equals(TITLE)) { - - if (second.equals(FEED)) { - state.getFeed().setTitle(textElement.getProcessedContent()); - } else if (second.equals(ENTRY)) { - state.getCurrentItem().setTitle( - textElement.getProcessedContent()); - } - } else if (top.equals(SUBTITLE)) { - if (second.equals(FEED)) { - state.getFeed().setDescription( - textElement.getProcessedContent()); - } - } else if (top.equals(CONTENT)) { - if (second.equals(ENTRY)) { - state.getCurrentItem().setDescription( - textElement.getProcessedContent()); - } - } else if (top.equals(UPDATED)) { - if (second.equals(ENTRY) - && state.getCurrentItem().getPubDate() == null) { - state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC3339Date(content)); - } - } else if (top.equals(PUBLISHED)) { - if (second.equals(ENTRY)) { - state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC3339Date(content)); - } - } else if (top.equals(IMAGE)) { - state.getFeed().setImage(new FeedImage(content, null)); - } - - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/util/SyndDateUtils.java b/app/src/main/java/de/danoeh/antennapod/syndication/util/SyndDateUtils.java deleted file mode 100644 index 3138f087a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/util/SyndDateUtils.java +++ /dev/null @@ -1,153 +0,0 @@ -package de.danoeh.antennapod.syndication.util; - -import android.util.Log; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** Parses several date formats. */ -public class SyndDateUtils { - private static final String TAG = "DateUtils"; - - private static final String[] RFC822DATES = { "dd MMM yy HH:mm:ss Z", }; - - /** RFC 3339 date format for UTC dates. */ - public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - - /** RFC 3339 date format for localtime dates with offset. */ - public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ"; - - private static ThreadLocal RFC822Formatter = new ThreadLocal() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC822DATES[0], Locale.US); - } - - }; - - private static ThreadLocal RFC3339Formatter = new ThreadLocal() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC3339UTC, Locale.US); - } - - }; - - public static Date parseRFC822Date(String date) { - Date result = null; - if (date.contains("PDT")) { - date = date.replace("PDT", "PST8PDT"); - } - if (date.contains(",")) { - // Remove day of the week - date = date.substring(date.indexOf(",") + 1).trim(); - } - SimpleDateFormat format = RFC822Formatter.get(); - for (int i = 0; i < RFC822DATES.length; i++) { - try { - format.applyPattern(RFC822DATES[i]); - result = format.parse(date); - break; - } catch (ParseException e) { - e.printStackTrace(); - } - } - if (result == null) { - Log.e(TAG, "Unable to parse feed date correctly"); - } - - return result; - } - - public static Date parseRFC3339Date(String date) { - Date result = null; - SimpleDateFormat format = RFC3339Formatter.get(); - boolean isLocal = date.endsWith("Z"); - if (date.contains(".")) { - // remove secfrac - int fracIndex = date.indexOf("."); - String first = date.substring(0, fracIndex); - String second = null; - if (isLocal) { - second = date.substring(date.length() - 1); - } else { - if (date.contains("+")) { - second = date.substring(date.indexOf("+")); - } else { - second = date.substring(date.indexOf("-")); - } - } - - date = first + second; - } - if (isLocal) { - try { - result = format.parse(date); - } catch (ParseException e) { - e.printStackTrace(); - } - } else { - format.applyPattern(RFC3339LOCAL); - // remove last colon - StringBuffer buf = new StringBuffer(date.length() - 1); - int colonIdx = date.lastIndexOf(':'); - for (int x = 0; x < date.length(); x++) { - if (x != colonIdx) - buf.append(date.charAt(x)); - } - String bufStr = buf.toString(); - try { - result = format.parse(bufStr); - } catch (ParseException e) { - e.printStackTrace(); - Log.e(TAG, "Unable to parse date"); - } finally { - format.applyPattern(RFC3339UTC); - } - - } - - return result; - - } - - /** - * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to - * milliseconds. - */ - public static long parseTimeString(final String time) { - String[] parts = time.split(":"); - long result = 0; - int idx = 0; - if (parts.length == 3) { - // string has hours - result += Integer.valueOf(parts[idx]) * 3600000L; - idx++; - } - result += Integer.valueOf(parts[idx]) * 60000L; - idx++; - result += (Float.valueOf(parts[idx])) * 1000L; - return result; - } - - public static String formatRFC822Date(Date date) { - SimpleDateFormat format = RFC822Formatter.get(); - return format.format(date); - } - - public static String formatRFC3339Local(Date date) { - SimpleDateFormat format = RFC3339Formatter.get(); - format.applyPattern(RFC3339LOCAL); - String result = format.format(date); - format.applyPattern(RFC3339UTC); - return result; - } - - public static String formatRFC3339UTC(Date date) { - SimpleDateFormat format = RFC3339Formatter.get(); - format.applyPattern(RFC3339UTC); - return format.format(date); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java b/app/src/main/java/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java deleted file mode 100644 index d0fa3a5fc..000000000 --- a/app/src/main/java/de/danoeh/antennapod/syndication/util/SyndTypeUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.danoeh.antennapod.syndication.util; - -import android.webkit.MimeTypeMap; -import org.apache.commons.io.FilenameUtils; - -/** Utility class for handling MIME-Types of enclosures */ -public class SyndTypeUtils { - - private final static String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*" - + "|" + "application/ogg"; - - private SyndTypeUtils() { - - } - - public static boolean enclosureTypeValid(String type) { - if (type == null) { - return false; - } else { - return type.matches(VALID_MIMETYPE); - } - } - - /** - * Should be used if mime-type of enclosure tag is not supported. This - * method will check if the mime-type of the file extension is supported. If - * the type is not supported, this method will return null. - */ - public static String getValidMimeTypeFromUrl(String url) { - if (url != null) { - String extension = FilenameUtils.getExtension(url); - if (extension != null) { - String type = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(extension); - if (type != null && enclosureTypeValid(type)) { - return type; - } - } - } - return null; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/ChapterUtils.java b/app/src/main/java/de/danoeh/antennapod/util/ChapterUtils.java deleted file mode 100644 index 9e1c50674..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/ChapterUtils.java +++ /dev/null @@ -1,261 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.util.comparator.ChapterStartTimeComparator; -import de.danoeh.antennapod.util.id3reader.ChapterReader; -import de.danoeh.antennapod.util.id3reader.ID3ReaderException; -import de.danoeh.antennapod.util.playback.Playable; -import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentChapterReader; -import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException; -import org.apache.commons.io.IOUtils; - -import java.io.*; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Collections; -import java.util.List; - -/** Utility class for getting chapter data from media files. */ -public class ChapterUtils { - private static final String TAG = "ChapterUtils"; - - private ChapterUtils() { - } - - /** - * Uses the download URL of a media object of a feeditem to read its ID3 - * chapters. - */ - public static void readID3ChaptersFromPlayableStreamUrl(Playable p) { - if (p != null && p.getStreamUrl() != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); - InputStream in = null; - try { - URL url = new URL(p.getStreamUrl()); - ChapterReader reader = new ChapterReader(); - - in = url.openStream(); - reader.readInputStream(in); - List chapters = reader.getChapters(); - - if (chapters != null) { - Collections - .sort(chapters, new ChapterStartTimeComparator()); - processChapters(chapters, p); - if (chaptersValid(chapters)) { - p.setChapters(chapters); - Log.i(TAG, "Chapters loaded"); - } else { - Log.e(TAG, "Chapter data was invalid"); - } - } else { - Log.i(TAG, "ChapterReader could not find any ID3 chapters"); - } - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (ID3ReaderException e) { - e.printStackTrace(); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } else { - Log.e(TAG, - "Unable to read ID3 chapters: media or download URL was null"); - } - } - - /** - * Uses the file URL of a media object of a feeditem to read its ID3 - * chapters. - */ - public static void readID3ChaptersFromPlayableFileUrl(Playable p) { - if (p != null && p.localFileAvailable() && p.getLocalMediaUrl() != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); - File source = new File(p.getLocalMediaUrl()); - if (source.exists()) { - ChapterReader reader = new ChapterReader(); - InputStream in = null; - - try { - in = new BufferedInputStream(new FileInputStream(source)); - reader.readInputStream(in); - List chapters = reader.getChapters(); - - if (chapters != null) { - Collections.sort(chapters, - new ChapterStartTimeComparator()); - processChapters(chapters, p); - if (chaptersValid(chapters)) { - p.setChapters(chapters); - Log.i(TAG, "Chapters loaded"); - } else { - Log.e(TAG, "Chapter data was invalid"); - } - } else { - Log.i(TAG, - "ChapterReader could not find any ID3 chapters"); - } - } catch (IOException e) { - e.printStackTrace(); - } catch (ID3ReaderException e) { - e.printStackTrace(); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } else { - Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist"); - } - } - } - - public static void readOggChaptersFromPlayableStreamUrl(Playable media) { - if (media != null && media.streamAvailable()) { - InputStream input = null; - try { - URL url = new URL(media.getStreamUrl()); - input = url.openStream(); - if (input != null) { - readOggChaptersFromInputStream(media, input); - } - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - IOUtils.closeQuietly(input); - } - } - } - - public static void readOggChaptersFromPlayableFileUrl(Playable media) { - if (media != null && media.getLocalMediaUrl() != null) { - File source = new File(media.getLocalMediaUrl()); - if (source.exists()) { - InputStream input = null; - try { - input = new BufferedInputStream(new FileInputStream(source)); - readOggChaptersFromInputStream(media, input); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } finally { - IOUtils.closeQuietly(input); - } - } - } - } - - private static void readOggChaptersFromInputStream(Playable p, - InputStream input) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Trying to read chapters from item with title " - + p.getEpisodeTitle()); - try { - VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); - reader.readInputStream(input); - List chapters = reader.getChapters(); - if (chapters != null) { - Collections.sort(chapters, new ChapterStartTimeComparator()); - processChapters(chapters, p); - if (chaptersValid(chapters)) { - p.setChapters(chapters); - Log.i(TAG, "Chapters loaded"); - } else { - Log.e(TAG, "Chapter data was invalid"); - } - } else { - Log.i(TAG, - "ChapterReader could not find any Ogg vorbis chapters"); - } - } catch (VorbisCommentReaderException e) { - e.printStackTrace(); - } - } - - /** Makes sure that chapter does a title and an item attribute. */ - private static void processChapters(List chapters, Playable p) { - for (int i = 0; i < chapters.size(); i++) { - Chapter c = chapters.get(i); - if (c.getTitle() == null) { - c.setTitle(Integer.toString(i)); - } - } - } - - private static boolean chaptersValid(List chapters) { - if (chapters.isEmpty()) { - return false; - } - for (Chapter c : chapters) { - if (c.getTitle() == null) { - return false; - } - if (c.getStart() < 0) { - return false; - } - } - return true; - } - - /** Calls getCurrentChapter with current position. */ - public static Chapter getCurrentChapter(Playable media) { - if (media.getChapters() != null) { - List chapters = media.getChapters(); - Chapter current = null; - if (chapters != null) { - current = chapters.get(0); - for (Chapter sc : chapters) { - if (sc.getStart() > media.getPosition()) { - break; - } else { - current = sc; - } - } - } - return current; - } else { - return null; - } - } - - public static void loadChaptersFromStreamUrl(Playable media) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting chapterLoader thread"); - ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media); - if (media.getChapters() == null) { - ChapterUtils.readOggChaptersFromPlayableStreamUrl(media); - } - - if (BuildConfig.DEBUG) - Log.d(TAG, "ChapterLoaderThread has finished"); - } - - public static void loadChaptersFromFileUrl(Playable media) { - if (media.localFileAvailable()) { - ChapterUtils.readID3ChaptersFromPlayableFileUrl(media); - if (media.getChapters() == null) { - ChapterUtils.readOggChaptersFromPlayableFileUrl(media); - } - } else { - Log.e(TAG, "Could not load chapters from file url: local file not available"); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/Converter.java b/app/src/main/java/de/danoeh/antennapod/util/Converter.java deleted file mode 100644 index f4c2b2f66..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/Converter.java +++ /dev/null @@ -1,103 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.util.Log; - -/** Provides methods for converting various units. */ -public final class Converter { - /** Class shall not be instantiated. */ - private Converter() { - } - - /** Logging tag. */ - private static final String TAG = "Converter"; - - - /** Indicates that the value is in the Byte range.*/ - private static final int B_RANGE = 0; - /** Indicates that the value is in the Kilobyte range.*/ - private static final int KB_RANGE = 1; - /** Indicates that the value is in the Megabyte range.*/ - private static final int MB_RANGE = 2; - /** Indicates that the value is in the Gigabyte range.*/ - private static final int GB_RANGE = 3; - /** Determines the length of the number for best readability.*/ - private static final int NUM_LENGTH = 1024; - - - private static final int HOURS_MIL = 3600000; - private static final int MINUTES_MIL = 60000; - private static final int SECONDS_MIL = 1000; - - /** Takes a byte-value and converts it into a more readable - * String. - * @param input The value to convert - * @return The converted String with a unit - * */ - public static String byteToString(final long input) { - int i = 0; - int result = 0; - - for (i = 0; i < GB_RANGE + 1; i++) { - result = (int) (input / Math.pow(1024, i)); - if (result < NUM_LENGTH) { - break; - } - } - - switch (i) { - case B_RANGE: - return result + " B"; - case KB_RANGE: - return result + " KB"; - case MB_RANGE: - return result + " MB"; - case GB_RANGE: - return result + " GB"; - default: - Log.e(TAG, "Error happened in byteToString"); - return "ERROR"; - } - } - - /** Converts milliseconds to a string containing hours, minutes and seconds */ - public static String getDurationStringLong(int duration) { - int h = duration / HOURS_MIL; - int rest = duration - h * HOURS_MIL; - int m = rest / MINUTES_MIL; - rest -= m * MINUTES_MIL; - int s = rest / SECONDS_MIL; - - return String.format("%02d:%02d:%02d", h, m, s); - } - - /** Converts milliseconds to a string containing hours and minutes */ - public static String getDurationStringShort(int duration) { - int h = duration / HOURS_MIL; - int rest = duration - h * HOURS_MIL; - int m = rest / MINUTES_MIL; - - return String.format("%02d:%02d", h, m); - } - - /** Converts long duration string (HH:MM:SS) to milliseconds. */ - public static int durationStringLongToMs(String input) { - String[] parts = input.split(":"); - if (parts.length != 3) { - return 0; - } - return Integer.valueOf(parts[0]) * 3600 * 1000 + - Integer.valueOf(parts[1]) * 60 * 1000 + - Integer.valueOf(parts[2]) * 1000; - } - - /** Converts short duration string (HH:MM) to milliseconds. */ - public static int durationStringShortToMs(String input) { - String[] parts = input.split(":"); - if (parts.length != 2) { - return 0; - } - return Integer.valueOf(parts[0]) * 3600 * 1000 + - Integer.valueOf(parts[1]) * 1000 * 60; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/DownloadError.java b/app/src/main/java/de/danoeh/antennapod/util/DownloadError.java deleted file mode 100644 index 1a64991a6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/DownloadError.java +++ /dev/null @@ -1,52 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.content.Context; -import de.danoeh.antennapod.R; - -/** Utility class for Download Errors. */ -public enum DownloadError { - SUCCESS(0, R.string.download_successful), - ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception), - ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type), - ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error), - ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown), - ERROR_IO_ERROR(5, R.string.download_error_io_error), - ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown), - ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown), - ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found), - ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error), - ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), - ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), - ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), - ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), - ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized); - - private final int code; - private final int resId; - - private DownloadError(int code, int resId) { - this.code = code; - this.resId = resId; - } - - /** Return DownloadError from its associated code. */ - public static DownloadError fromCode(int code) { - for (DownloadError reason : values()) { - if (reason.getCode() == code) { - return reason; - } - } - throw new IllegalArgumentException("unknown code: " + code); - } - - /** Get machine-readable code. */ - public int getCode() { - return code; - } - - /** Get a human-readable string. */ - public String getErrorString(Context context) { - return context.getString(resId); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/DuckType.java b/app/src/main/java/de/danoeh/antennapod/util/DuckType.java deleted file mode 100644 index 163110418..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/DuckType.java +++ /dev/null @@ -1,117 +0,0 @@ -/* Adapted from: http://thinking-in-code.blogspot.com/2008/11/duck-typing-in-java-using-dynamic.html */ - -package de.danoeh.antennapod.util; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -import de.danoeh.antennapod.BuildConfig; - -/** - * Allows "duck typing" or dynamic invocation based on method signature rather - * than type hierarchy. In other words, rather than checking whether something - * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck. - * - * To use first use the coerce static method to indicate the object you want to - * do Duck Typing for, then specify an interface to the to method which you want - * to coerce the type to, e.g: - * - * public interface Foo { void aMethod(); } class Bar { ... public void - * aMethod() { ... } ... } Bar bar = ...; Foo foo = - * DuckType.coerce(bar).to(Foo.class); foo.aMethod(); - * - * - */ -public class DuckType { - - private final Object objectToCoerce; - - private DuckType(Object objectToCoerce) { - this.objectToCoerce = objectToCoerce; - } - - private class CoercedProxy implements InvocationHandler { - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - Method delegateMethod = findMethodBySignature(method); - assert delegateMethod != null; - return delegateMethod.invoke(DuckType.this.objectToCoerce, args); - } - } - - /** - * Specify the duck typed object to coerce. - * - * @param object - * the object to coerce - * @return - */ - public static DuckType coerce(Object object) { - return new DuckType(object); - } - - /** - * Coerce the Duck Typed object to the given interface providing it - * implements all the necessary methods. - * - * @param - * @param iface - * @return an instance of the given interface that wraps the duck typed - * class - * @throws ClassCastException - * if the object being coerced does not implement all the - * methods in the given interface. - */ - @SuppressWarnings({ "rawtypes", "unchecked" }) - public T to(Class iface) { - if (BuildConfig.DEBUG && !iface.isInterface()) throw new AssertionError("cannot coerce object to a class, must be an interface"); - if (isA(iface)) { - return (T) iface.cast(objectToCoerce); - } - if (quacksLikeA(iface)) { - return generateProxy(iface); - } - throw new ClassCastException("Could not coerce object of type " + objectToCoerce.getClass() + " to " + iface); - } - - @SuppressWarnings("rawtypes") - private boolean isA(Class iface) { - return objectToCoerce.getClass().isInstance(iface); - } - - /** - * Determine whether the duck typed object can be used with the given - * interface. - * - * @param Type - * of the interface to check. - * @param iface - * Interface class to check - * @return true if the object will support all the methods in the interface, - * false otherwise. - */ - @SuppressWarnings("rawtypes") - public boolean quacksLikeA(Class iface) { - for (Method method : iface.getMethods()) { - if (findMethodBySignature(method) == null) { - return false; - } - } - return true; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private T generateProxy(Class iface) { - return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new CoercedProxy()); - } - - private Method findMethodBySignature(Method method) { - try { - return objectToCoerce.getClass().getMethod(method.getName(), method.getParameterTypes()); - } catch (NoSuchMethodException e) { - return null; - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/util/EpisodeFilter.java b/app/src/main/java/de/danoeh/antennapod/util/EpisodeFilter.java deleted file mode 100644 index 115913bca..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/EpisodeFilter.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.danoeh.antennapod.util; - -import de.danoeh.antennapod.feed.FeedItem; - -import java.util.ArrayList; -import java.util.List; - -public class EpisodeFilter { - private EpisodeFilter() { - - } - - /** Return a copy of the itemlist without items which have no media. */ - public static ArrayList getEpisodeList(List items) { - ArrayList episodes = new ArrayList(items); - for (FeedItem item : items) { - if (item.getMedia() == null) { - episodes.remove(item); - } - } - return episodes; - } - - public static int countItemsWithEpisodes(List items) { - int count = 0; - for (FeedItem item : items) { - if (item.getMedia() != null) { - count++; - } - } - return count; - } - - public static FeedItem accessEpisodeByIndex(List items, - int position) { - int count = 0; - for (FeedItem item : items) { - - if (item.getMedia() != null) { - if (count == position) { - return item; - } else { - count++; - } - } - } - return null; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/FeedtitleComparator.java b/app/src/main/java/de/danoeh/antennapod/util/FeedtitleComparator.java deleted file mode 100644 index 112b6678d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/FeedtitleComparator.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.danoeh.antennapod.util; - -import de.danoeh.antennapod.feed.Feed; - -import java.util.Comparator; - -/** Compares the title of two feeds for sorting. */ -public class FeedtitleComparator implements Comparator { - - @Override - public int compare(Feed lhs, Feed rhs) { - return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/FileNameGenerator.java b/app/src/main/java/de/danoeh/antennapod/util/FileNameGenerator.java deleted file mode 100644 index 702df62b8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/FileNameGenerator.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.danoeh.antennapod.util; - -import java.util.Arrays; - -/** Generates valid filenames for a given string. */ -public class FileNameGenerator { - - private static final char[] ILLEGAL_CHARACTERS = { '/', '\\', '?', '%', - '*', ':', '|', '"', '<', '>' }; - static { - Arrays.sort(ILLEGAL_CHARACTERS); - } - - private FileNameGenerator() { - - } - - /** - * This method will return a new string that doesn't contain any illegal - * characters of the given string. - */ - public static String generateFileName(String string) { - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < string.length(); i++) { - char c = string.charAt(i); - if (Arrays.binarySearch(ILLEGAL_CHARACTERS, c) < 0) { - builder.append(c); - } - } - return builder.toString().replaceFirst(" *$",""); - } - - public static long generateLong(final String str) { - return str.hashCode(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/InvalidFeedException.java b/app/src/main/java/de/danoeh/antennapod/util/InvalidFeedException.java deleted file mode 100644 index 50adae216..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/InvalidFeedException.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.util; - -/** Thrown if a feed has invalid attribute values. */ -public class InvalidFeedException extends Exception { - - public InvalidFeedException() { - } - - public InvalidFeedException(String detailMessage) { - super(detailMessage); - } - - public InvalidFeedException(Throwable throwable) { - super(throwable); - } - - public InvalidFeedException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/LangUtils.java b/app/src/main/java/de/danoeh/antennapod/util/LangUtils.java deleted file mode 100644 index e6e1d8399..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/LangUtils.java +++ /dev/null @@ -1,120 +0,0 @@ -package de.danoeh.antennapod.util; - -import java.nio.charset.Charset; -import java.util.HashMap; - -public class LangUtils { - public static final Charset UTF_8 = Charset.forName("UTF-8"); - - private static HashMap languages; - static { - languages = new HashMap(); - languages.put("af", "Afrikaans"); - languages.put("sq", "Albanian"); - languages.put("sq", "Albanian"); - languages.put("eu", "Basque"); - languages.put("be", "Belarusian"); - languages.put("bg", "Bulgarian"); - languages.put("ca", "Catalan"); - languages.put("Chinese (Simplified)", "zh-cn"); - languages.put("Chinese (Traditional)", "zh-tw"); - languages.put("hr", "Croatian"); - languages.put("cs", "Czech"); - languages.put("da", "Danish"); - languages.put("nl", "Dutch"); - languages.put("nl-be", "Dutch (Belgium)"); - languages.put("nl-nl", "Dutch (Netherlands)"); - languages.put("en", "English"); - languages.put("en-au", "English (Australia)"); - languages.put("en-bz", "English (Belize)"); - languages.put("en-ca", "English (Canada)"); - languages.put("en-ie", "English (Ireland)"); - languages.put("en-jm", "English (Jamaica)"); - languages.put("en-nz", "English (New Zealand)"); - languages.put("en-ph", "English (Phillipines)"); - languages.put("en-za", "English (South Africa)"); - languages.put("en-tt", "English (Trinidad)"); - languages.put("en-gb", "English (United Kingdom)"); - languages.put("en-us", "English (United States)"); - languages.put("en-zw", "English (Zimbabwe)"); - languages.put("et", "Estonian"); - languages.put("fo", "Faeroese"); - languages.put("fi", "Finnish"); - languages.put("fr", "French"); - languages.put("fr-be", "French (Belgium)"); - languages.put("fr-ca", "French (Canada)"); - languages.put("fr-fr", "French (France)"); - languages.put("fr-lu", "French (Luxembourg)"); - languages.put("fr-mc", "French (Monaco)"); - languages.put("fr-ch", "French (Switzerland)"); - languages.put("gl", "Galician"); - languages.put("gd", "Gaelic"); - languages.put("de", "German"); - languages.put("de-at", "German (Austria)"); - languages.put("de-de", "German (Germany)"); - languages.put("de-li", "German (Liechtenstein)"); - languages.put("de-lu", "German (Luxembourg)"); - languages.put("de-ch", "German (Switzerland)"); - languages.put("el", "Greek"); - languages.put("haw", "Hawaiian"); - languages.put("hu", "Hungarian"); - languages.put("is", "Icelandic"); - languages.put("in", "Indonesian"); - languages.put("ga", "Irish"); - languages.put("it", "Italian"); - languages.put("it-it", "Italian (Italy)"); - languages.put("it-ch", "Italian (Switzerland)"); - languages.put("ja", "Japanese"); - languages.put("ko", "Korean"); - languages.put("mk", "Macedonian"); - languages.put("no", "Norwegian"); - languages.put("pl", "Polish"); - languages.put("pt", "Portugese"); - languages.put("pt-br", "Portugese (Brazil)"); - languages.put("pt-pt", "Portugese (Portugal"); - languages.put("ro", "Romanian"); - languages.put("ro-mo", "Romanian (Moldova)"); - languages.put("ro-ro", "Romanian (Romania"); - languages.put("ru", "Russian"); - languages.put("ru-mo", "Russian (Moldova)"); - languages.put("ru-ru", "Russian (Russia)"); - languages.put("sr", "Serbian"); - languages.put("sk", "Slovak"); - languages.put("sl", "Slovenian"); - languages.put("es", "Spanish"); - languages.put("es-ar", "Spanish (Argentinia)"); - languages.put("es=bo", "Spanish (Bolivia)"); - languages.put("es-cl", "Spanish (Chile)"); - languages.put("es-co", "Spanish (Colombia)"); - languages.put("es-cr", "Spanish (Costa Rica)"); - languages.put("es-do", "Spanish (Dominican Republic)"); - languages.put("es-ec", "Spanish (Ecuador)"); - languages.put("es-sv", "Spanish (El Salvador)"); - languages.put("es-gt", "Spanish (Guatemala)"); - languages.put("es-hn", "Spanish (Honduras)"); - languages.put("es-mx", "Spanish (Mexico)"); - languages.put("es-ni", "Spanish (Nicaragua)"); - languages.put("es-pa", "Spanish (Panama)"); - languages.put("es-py", "Spanish (Paraguay)"); - languages.put("es-pe", "Spanish (Peru)"); - languages.put("es-pr", "Spanish (Puerto Rico)"); - languages.put("es-es", "Spanish (Spain)"); - languages.put("es-uy", "Spanish (Uruguay)"); - languages.put("es-ve", "Spanish (Venezuela)"); - languages.put("sv", "Swedish"); - languages.put("sv-fi", "Swedish (Finland)"); - languages.put("sv-se", "Swedish (Sweden)"); - languages.put("tr", "Turkish"); - languages.put("uk", "Ukranian"); - } - - /** Finds language string for key or returns the language key if it can't be found. */ - public static String getLanguageString(String key) { - String language = languages.get(key); - if (language != null) { - return language; - } else { - return key; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/NetworkUtils.java b/app/src/main/java/de/danoeh/antennapod/util/NetworkUtils.java deleted file mode 100644 index 0c8065e94..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/NetworkUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.preferences.UserPreferences; - -import java.util.Arrays; -import java.util.List; - -public class NetworkUtils { - private static final String TAG = "NetworkUtils"; - - private NetworkUtils() { - - } - - /** - * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for - * automatic downloads is disabled or the device is connected to a Wi-Fi - * network that is on the 'selected networks' list of the Wi-Fi filter for - * automatic downloads and false otherwise. - * */ - public static boolean autodownloadNetworkAvailable(Context context) { - ConnectivityManager cm = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - if (networkInfo != null) { - if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Device is connected to Wi-Fi"); - if (networkInfo.isConnected()) { - if (!UserPreferences.isEnableAutodownloadWifiFilter()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Auto-dl filter is disabled"); - return true; - } else { - WifiManager wm = (WifiManager) context - .getSystemService(Context.WIFI_SERVICE); - WifiInfo wifiInfo = wm.getConnectionInfo(); - List selectedNetworks = Arrays - .asList(UserPreferences - .getAutodownloadSelectedNetworks()); - if (selectedNetworks.contains(Integer.toString(wifiInfo - .getNetworkId()))) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Current network is on the selected networks list"); - return true; - } - } - } - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Network for auto-dl is not available"); - return false; - } - - public static boolean networkAvailable(Context context) { - ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - return info != null && info.isConnected(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/QueueAccess.java b/app/src/main/java/de/danoeh/antennapod/util/QueueAccess.java deleted file mode 100644 index 7a1c7fef2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/QueueAccess.java +++ /dev/null @@ -1,93 +0,0 @@ -package de.danoeh.antennapod.util; - -import de.danoeh.antennapod.feed.FeedItem; - -import java.util.Iterator; -import java.util.List; - -/** - * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that - * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List. - * QueueAccess provides an interface for accessing the queue without having to care about the type of the queue - * representation. - */ -public abstract class QueueAccess { - /** - * Returns true if the item is in the queue, false otherwise. - */ - public abstract boolean contains(long id); - - /** - * Removes the item from the queue. - * - * @return true if the queue was modified by this operation. - */ - public abstract boolean remove(long id); - - private QueueAccess() { - - } - - public static QueueAccess IDListAccess(final List ids) { - return new QueueAccess() { - @Override - public boolean contains(long id) { - return (ids != null) && ids.contains(id); - } - - @Override - public boolean remove(long id) { - return ids.remove(id); - } - - - }; - } - - public static QueueAccess ItemListAccess(final List items) { - return new QueueAccess() { - @Override - public boolean contains(long id) { - if (items == null) { - return false; - } - for (FeedItem item : items) { - if (item.getId() == id) { - return true; - } - } - return false; - } - - @Override - public boolean remove(long id) { - Iterator it = items.iterator(); - FeedItem item; - while (it.hasNext()) { - item = it.next(); - if (item.getId() == id) { - it.remove(); - return true; - } - } - return false; - } - }; - } - - public static QueueAccess NotInQueueAccess() { - return new QueueAccess() { - @Override - public boolean contains(long id) { - return false; - } - - @Override - public boolean remove(long id) { - return false; - } - }; - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/ShareUtils.java b/app/src/main/java/de/danoeh/antennapod/util/ShareUtils.java deleted file mode 100644 index 941fc62ae..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/ShareUtils.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.content.Context; -import android.content.Intent; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; - -/** Utility methods for sharing data */ -public class ShareUtils { - private static final String TAG = "ShareUtils"; - - private ShareUtils() {} - - public static void shareLink(Context context, String link) { - Intent i = new Intent(Intent.ACTION_SEND); - i.setType("text/plain"); - i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL"); - i.putExtra(Intent.EXTRA_TEXT, link); - context.startActivity(Intent.createChooser(i, "Share URL")); - } - - public static void shareFeedItemLink(Context context, FeedItem item) { - shareLink(context, item.getLink()); - } - - public static void shareFeedDownloadLink(Context context, Feed feed) { - shareLink(context, feed.getDownload_url()); - } - - public static void shareFeedlink(Context context, Feed feed) { - shareLink(context, feed.getLink()); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/ShownotesProvider.java b/app/src/main/java/de/danoeh/antennapod/util/ShownotesProvider.java deleted file mode 100644 index 8345ca34d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/ShownotesProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.danoeh.antennapod.util; - -import java.util.concurrent.Callable; - -/** - * Created by daniel on 04.08.13. - */ -public interface ShownotesProvider { - /** - * Loads shownotes. If the shownotes have to be loaded from a file or from a - * database, it should be done in a separate thread. After the shownotes - * have been loaded, callback.onShownotesLoaded should be called. - */ - public Callable loadShownotes(); - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/StorageUtils.java b/app/src/main/java/de/danoeh/antennapod/util/StorageUtils.java deleted file mode 100644 index ff0bde280..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/StorageUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.StatFs; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.activity.StorageErrorActivity; -import de.danoeh.antennapod.preferences.UserPreferences; - -import java.io.File; - -/** Utility functions for handling storage errors */ -public class StorageUtils { - private static final String TAG = "StorageUtils"; - - public static boolean storageAvailable(Context context) { - File dir = UserPreferences.getDataFolder(context, null); - if (dir != null) { - return dir.exists() && dir.canRead() && dir.canWrite(); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Storage not available: data folder is null"); - return false; - } - } - - /** - * Checks if external storage is available. If external storage isn't - * available, the current activity is finsished an an error activity is - * launched. - * - * @param activity - * the activity which would be finished if no storage is - * available - * @return true if external storage is available - */ - public static boolean checkStorageAvailability(Activity activity) { - boolean storageAvailable = storageAvailable(activity); - if (!storageAvailable) { - activity.finish(); - activity.startActivity(new Intent(activity, - StorageErrorActivity.class)); - } - return storageAvailable; - } - - /** Get the number of free bytes that are available on the external storage. */ - public static long getFreeSpaceAvailable() { - StatFs stat = new StatFs(UserPreferences.getDataFolder( - PodcastApp.getInstance(), null).getAbsolutePath()); - long availableBlocks; - long blockSize; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - availableBlocks = stat.getAvailableBlocksLong(); - blockSize = stat.getBlockSizeLong(); - } else { - availableBlocks = stat.getAvailableBlocks(); - blockSize = stat.getBlockSize(); - } - return availableBlocks * blockSize; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/ThemeUtils.java b/app/src/main/java/de/danoeh/antennapod/util/ThemeUtils.java deleted file mode 100644 index 8e593f3fb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/ThemeUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.util.Log; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.preferences.UserPreferences; - -public class ThemeUtils { - private static final String TAG = "ThemeUtils"; - - public static int getSelectionBackgroundColor() { - switch (UserPreferences.getTheme()) { - case R.style.Theme_AntennaPod_Dark: - return R.color.selection_background_color_dark; - case R.style.Theme_AntennaPod_Light: - return R.color.selection_background_color_light; - default: - Log.e(TAG, - "getSelectionBackgroundColor could not match the current theme to any color!"); - return R.color.selection_background_color_light; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/URIUtil.java b/app/src/main/java/de/danoeh/antennapod/util/URIUtil.java deleted file mode 100644 index 5af40d591..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/URIUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; - -/** - * Utility methods for dealing with URL encoding. - */ -public class URIUtil { - private static final String TAG = "URIUtil"; - - private URIUtil() {} - - public static URI getURIFromRequestUrl(String source) { - // try without encoding the URI - try { - return new URI(source); - } catch (URISyntaxException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "Source is not encoded, encoding now"); - } - try { - URL url = new URL(source); - return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); - } catch (MalformedURLException e) { - throw new IllegalArgumentException(e); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/URLChecker.java b/app/src/main/java/de/danoeh/antennapod/util/URLChecker.java deleted file mode 100644 index 9997daaf7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/URLChecker.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.danoeh.antennapod.util; - -import android.util.Log; - -import org.apache.commons.lang3.StringUtils; - -import de.danoeh.antennapod.BuildConfig; - -/** - * Provides methods for checking and editing a URL. - */ -public final class URLChecker { - - /** - * Class shall not be instantiated. - */ - private URLChecker() { - } - - /** - * Logging tag. - */ - private static final String TAG = "URLChecker"; - - /** - * Checks if URL is valid and modifies it if necessary. - * - * @param url The url which is going to be prepared - * @return The prepared url - */ - public static String prepareURL(String url) { - StringBuilder builder = new StringBuilder(); - url = StringUtils.trim(url); - if (url.startsWith("feed://")) { - if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://"); - url = url.replaceFirst("feed://", "http://"); - } else if (url.startsWith("pcast://")) { - if (BuildConfig.DEBUG) Log.d(TAG, "Replacing pcast:// with http://"); - url = url.replaceFirst("pcast://", "http://"); - } else if (url.startsWith("itpc")) { - if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://"); - url = url.replaceFirst("itpc://", "http://"); - } else if (!(url.startsWith("http://") || url.startsWith("https://"))) { - if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL"); - builder.append("http://"); - } - builder.append(url); - - return builder.toString(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/UndoBarController.java b/app/src/main/java/de/danoeh/antennapod/util/UndoBarController.java deleted file mode 100644 index 332cc22e0..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/UndoBarController.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2012 Roman Nurik - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.danoeh.antennapod.util; - -import android.os.Bundle; -import android.os.Handler; -import android.os.Parcelable; -import android.text.TextUtils; -import android.view.View; -import android.widget.TextView; -import com.nineoldandroids.animation.Animator; -import com.nineoldandroids.animation.AnimatorListenerAdapter; -import com.nineoldandroids.view.ViewHelper; -import com.nineoldandroids.view.ViewPropertyAnimator; -import de.danoeh.antennapod.R; - -import static com.nineoldandroids.view.ViewPropertyAnimator.animate; - -public class UndoBarController { - private View mBarView; - private TextView mMessageView; - private ViewPropertyAnimator mBarAnimator; - private Handler mHideHandler = new Handler(); - - private UndoListener mUndoListener; - - // State objects - private Parcelable mUndoToken; - private CharSequence mUndoMessage; - - public interface UndoListener { - void onUndo(Parcelable token); - } - - public UndoBarController(View undoBarView, UndoListener undoListener) { - mBarView = undoBarView; - mBarAnimator = animate(mBarView); - mUndoListener = undoListener; - - mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message); - mBarView.findViewById(R.id.undobar_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - hideUndoBar(false); - mUndoListener.onUndo(mUndoToken); - } - }); - - hideUndoBar(true); - } - - public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) { - mUndoToken = undoToken; - mUndoMessage = message; - mMessageView.setText(mUndoMessage); - - mHideHandler.removeCallbacks(mHideRunnable); - mHideHandler.postDelayed(mHideRunnable, - mBarView.getResources().getInteger(R.integer.undobar_hide_delay)); - - mBarView.setVisibility(View.VISIBLE); - if (immediate) { - ViewHelper.setAlpha(mBarView, 1); - } else { - mBarAnimator.cancel(); - mBarAnimator - .alpha(1) - .setDuration( - mBarView.getResources() - .getInteger(android.R.integer.config_shortAnimTime)) - .setListener(null); - } - } - - public void hideUndoBar(boolean immediate) { - mHideHandler.removeCallbacks(mHideRunnable); - if (immediate) { - mBarView.setVisibility(View.GONE); - ViewHelper.setAlpha(mBarView, 0); - mUndoMessage = null; - mUndoToken = null; - - } else { - mBarAnimator.cancel(); - mBarAnimator - .alpha(0) - .setDuration(mBarView.getResources() - .getInteger(android.R.integer.config_shortAnimTime)) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mBarView.setVisibility(View.GONE); - mUndoMessage = null; - mUndoToken = null; - } - }); - } - } - - public void onSaveInstanceState(Bundle outState) { - outState.putCharSequence("undo_message", mUndoMessage); - outState.putParcelable("undo_token", mUndoToken); - } - - public void onRestoreInstanceState(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mUndoMessage = savedInstanceState.getCharSequence("undo_message"); - mUndoToken = savedInstanceState.getParcelable("undo_token"); - - if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) { - showUndoBar(true, mUndoMessage, mUndoToken); - } - } - } - - private Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - hideUndoBar(false); - } - }; -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java b/app/src/main/java/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java deleted file mode 100644 index bfc2fd057..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/comparator/ChapterStartTimeComparator.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.danoeh.antennapod.util.comparator; - -import de.danoeh.antennapod.feed.Chapter; - -import java.util.Comparator; - -public class ChapterStartTimeComparator implements Comparator { - - @Override - public int compare(Chapter lhs, Chapter rhs) { - if (lhs.getStart() == rhs.getStart()) { - return 0; - } else if (lhs.getStart() < rhs.getStart()) { - return -1; - } else { - return 1; - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java b/app/src/main/java/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java deleted file mode 100644 index 14b8f1194..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.danoeh.antennapod.util.comparator; - -import de.danoeh.antennapod.service.download.DownloadStatus; - -import java.util.Comparator; - -/** Compares the completion date of two Downloadstatus objects. */ -public class DownloadStatusComparator implements Comparator { - - @Override - public int compare(DownloadStatus lhs, DownloadStatus rhs) { - return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java b/app/src/main/java/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java deleted file mode 100644 index f92c23d05..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/comparator/FeedItemPubdateComparator.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.util.comparator; - -import de.danoeh.antennapod.feed.FeedItem; - -import java.util.Comparator; - -/** Compares the pubDate of two FeedItems for sorting*/ -public class FeedItemPubdateComparator implements Comparator { - - /** Returns a new instance of this comparator in reverse order. - public static FeedItemPubdateComparator newInstance() { - FeedItemPubdateComparator - }*/ - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - return rhs.getPubDate().compareTo(lhs.getPubDate()); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java b/app/src/main/java/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java deleted file mode 100644 index 0147e0cdc..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/comparator/PlaybackCompletionDateComparator.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.util.comparator; - -import de.danoeh.antennapod.feed.FeedItem; - -import java.util.Comparator; - -public class PlaybackCompletionDateComparator implements Comparator { - - public int compare(FeedItem lhs, FeedItem rhs) { - if (lhs.getMedia() != null - && lhs.getMedia().getPlaybackCompletionDate() != null - && rhs.getMedia() != null - && rhs.getMedia().getPlaybackCompletionDate() != null) { - return rhs.getMedia().getPlaybackCompletionDate() - .compareTo(lhs.getMedia().getPlaybackCompletionDate()); - } - return 0; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java b/app/src/main/java/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java deleted file mode 100644 index 02b084a01..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/comparator/SearchResultValueComparator.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.danoeh.antennapod.util.comparator; - -import de.danoeh.antennapod.feed.SearchResult; - -import java.util.Comparator; - -public class SearchResultValueComparator implements Comparator { - - @Override - public int compare(SearchResult lhs, SearchResult rhs) { - return rhs.getValue() - lhs.getValue(); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java b/app/src/main/java/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java deleted file mode 100644 index 4586cea87..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/exception/MediaFileNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.danoeh.antennapod.util.exception; - -import de.danoeh.antennapod.feed.FeedMedia; - -public class MediaFileNotFoundException extends Exception { - private static final long serialVersionUID = 1L; - - private FeedMedia media; - - public MediaFileNotFoundException(String msg, FeedMedia media) { - super(msg); - this.media = media; - } - - public MediaFileNotFoundException(FeedMedia media) { - super(); - this.media = media; - } - - public FeedMedia getMedia() { - return media; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java b/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java deleted file mode 100644 index eda83b7aa..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrServiceCreator.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.danoeh.antennapod.util.flattr; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import org.shredzone.flattr4j.FlattrFactory; -import org.shredzone.flattr4j.FlattrService; -import org.shredzone.flattr4j.oauth.AccessToken; - -/** Ensures that only one instance of the FlattrService class exists at a time */ - -public class FlattrServiceCreator { - public static final String TAG = "FlattrServiceCreator"; - - private static volatile FlattrService flattrService; - - public static FlattrService getService(AccessToken token) { - return FlattrFactory.getInstance().createFlattrService(token); - } - - public static void deleteFlattrService() { - if (BuildConfig.DEBUG) Log.d(TAG, "Deleting service instance"); - flattrService = null; - } -} - diff --git a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrStatus.java b/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrStatus.java deleted file mode 100644 index a1d6d3bc4..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrStatus.java +++ /dev/null @@ -1,68 +0,0 @@ -package de.danoeh.antennapod.util.flattr; - -import java.util.Calendar; - -public class FlattrStatus { - public static final int STATUS_UNFLATTERED = 0; - public static final int STATUS_QUEUE = 1; - public static final int STATUS_FLATTRED = 2; - - private int status = STATUS_UNFLATTERED; - private Calendar lastFlattred; - - public FlattrStatus() { - status = STATUS_UNFLATTERED; - lastFlattred = Calendar.getInstance(); - } - - public FlattrStatus(long status) { - lastFlattred = Calendar.getInstance(); - fromLong(status); - } - - public void setFlattred() { - status = STATUS_FLATTRED; - lastFlattred = Calendar.getInstance(); - } - - public void setUnflattred() { - status = STATUS_UNFLATTERED; - } - - public boolean getUnflattred() { - return status == STATUS_UNFLATTERED; - } - - public void setFlattrQueue() { - if (flattrable()) - status = STATUS_QUEUE; - } - - public void fromLong(long status) { - if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) - this.status = (int) status; - else { - this.status = STATUS_FLATTRED; - lastFlattred.setTimeInMillis(status); - } - } - - public long toLong() { - if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) - return status; - else { - return lastFlattred.getTimeInMillis(); - } - } - - public boolean flattrable() { - Calendar firstOfMonth = Calendar.getInstance(); - firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); - - return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) ); - } - - public boolean getFlattrQueue() { - return status == STATUS_QUEUE; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrThing.java b/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrThing.java deleted file mode 100644 index f17ef1d83..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrThing.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.danoeh.antennapod.util.flattr; - -public interface FlattrThing { - public String getTitle(); - public String getPaymentLink(); - public FlattrStatus getFlattrStatus(); -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrUtils.java b/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrUtils.java deleted file mode 100644 index 3e2ea3132..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/flattr/FlattrUtils.java +++ /dev/null @@ -1,305 +0,0 @@ -package de.danoeh.antennapod.util.flattr; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.preference.PreferenceManager; -import android.util.Log; - -import org.apache.commons.lang3.StringUtils; -import org.shredzone.flattr4j.FlattrService; -import org.shredzone.flattr4j.exception.FlattrException; -import org.shredzone.flattr4j.model.Flattr; -import org.shredzone.flattr4j.model.Thing; -import org.shredzone.flattr4j.oauth.AccessToken; -import org.shredzone.flattr4j.oauth.AndroidAuthenticator; -import org.shredzone.flattr4j.oauth.Scope; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.EnumSet; -import java.util.List; -import java.util.TimeZone; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.FlattrAuthActivity; -import de.danoeh.antennapod.asynctask.FlattrTokenFetcher; -import de.danoeh.antennapod.storage.DBWriter; - -/** - * Utility methods for doing something with flattr. - */ - -public class FlattrUtils { - private static final String TAG = "FlattrUtils"; - - private static final String HOST_NAME = "de.danoeh.antennapod"; - - private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken"; - - // Flattr URL for this app. - public static final String APP_URL = "http://antennapod.com"; - // Human-readable flattr-page. - public static final String APP_LINK = "https://flattr.com/thing/745609/"; - public static final String APP_THING_ID = "745609"; - - private static volatile AccessToken cachedToken; - - private static AndroidAuthenticator createAuthenticator() { - return new AndroidAuthenticator(HOST_NAME, BuildConfig.FLATTR_APP_KEY, - BuildConfig.FLATTR_APP_SECRET); - } - - public static void startAuthProcess(Context context) throws FlattrException { - AndroidAuthenticator auth = createAuthenticator(); - auth.setScope(EnumSet.of(Scope.FLATTR)); - Intent intent = auth.createAuthenticateIntent(); - context.startActivity(intent); - } - - private static AccessToken retrieveToken() { - if (cachedToken == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Retrieving access token"); - String token = PreferenceManager.getDefaultSharedPreferences( - PodcastApp.getInstance()) - .getString(PREF_ACCESS_TOKEN, null); - if (token != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Found access token. Caching."); - cachedToken = new AccessToken(token); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No access token found"); - return null; - } - } - return cachedToken; - - } - - /** - * Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty - */ - public static boolean hasAPICredentials() { - return StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_KEY) - && StringUtils.isNotEmpty(BuildConfig.FLATTR_APP_SECRET); - } - - public static boolean hasToken() { - return retrieveToken() != null; - } - - public static void storeToken(AccessToken token) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Storing token"); - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(PodcastApp.getInstance()).edit(); - if (token != null) { - editor.putString(PREF_ACCESS_TOKEN, token.getToken()); - } else { - editor.putString(PREF_ACCESS_TOKEN, null); - } - editor.commit(); - cachedToken = token; - } - - public static void deleteToken() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting flattr token"); - storeToken(null); - } - - public static Thing getAppThing(Context context) { - FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); - try { - Thing thing = fs.getThing(Thing.withId(APP_THING_ID)); - return thing; - } catch (FlattrException e) { - e.printStackTrace(); - showErrorDialog(context, e.getMessage()); - return null; - } - } - - public static void clickUrl(Context context, String url) - throws FlattrException { - if (hasToken()) { - FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); - fs.click(url); - } else { - Log.e(TAG, "clickUrl was called with null access token"); - } - } - - public static List retrieveFlattredThings() - throws FlattrException { - ArrayList myFlattrs = new ArrayList(); - - if (hasToken()) { - FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); - - Calendar firstOfMonth = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - firstOfMonth.set(Calendar.MILLISECOND, 0); - firstOfMonth.set(Calendar.SECOND, 0); - firstOfMonth.set(Calendar.MINUTE, 0); - firstOfMonth.set(Calendar.HOUR_OF_DAY, 0); - firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); - - Date firstOfMonthDate = firstOfMonth.getTime(); - - // subscriptions some times get flattrd slightly before midnight - give it an hour leeway - firstOfMonthDate = new Date(firstOfMonthDate.getTime() - 60 * 60 * 1000); - - final int FLATTR_COUNT = 30; - final int FLATTR_MAXPAGE = 5; - - for (int page = 0; page < FLATTR_MAXPAGE; page++) { - for (Flattr fl : fs.getMyFlattrs(FLATTR_COUNT, page)) { - if (fl.getCreated().after(firstOfMonthDate)) - myFlattrs.add(fl); - else - break; - } - } - - if (BuildConfig.DEBUG) { - Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate); - - for (Flattr fl : myFlattrs) { - Thing thing = fl.getThing(); - Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated()); - } - } - - } else { - Log.e(TAG, "retrieveFlattrdThings was called with null access token"); - } - - return myFlattrs; - } - - public static void handleCallback(Context context, Uri uri) { - AndroidAuthenticator auth = createAuthenticator(); - new FlattrTokenFetcher(context, auth, uri).executeAsync(); - } - - public static void revokeAccessToken(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Revoking access token"); - deleteToken(); - FlattrServiceCreator.deleteFlattrService(); - showRevokeDialog(context); - DBWriter.clearAllFlattrStatus(context); - } - - // ------------------------------------------------ DIALOGS - - public static void showRevokeDialog(final Context context) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.access_revoked_title); - builder.setMessage(R.string.access_revoked_info); - builder.setNeutralButton(android.R.string.ok, new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); - builder.create().show(); - } - - /** - * Opens a dialog that ask the user to either connect the app with flattr or to be redirected to - * the thing's website. - * If no API credentials are available, the user will immediately be redirected to the thing's website. - * */ - public static void showNoTokenDialogOrRedirect(final Context context, final String url) { - if (hasAPICredentials()) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.no_flattr_token_title); - builder.setMessage(R.string.no_flattr_token_msg); - builder.setPositiveButton(R.string.authenticate_now_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - context.startActivity(new Intent(context, - FlattrAuthActivity.class)); - } - - } - ); - - builder.setNegativeButton(R.string.visit_website_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, - uri)); - } - - } - ); - builder.create().show(); - } else { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - } - } - - public static void showForbiddenDialog(final Context context, - final String url) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.action_forbidden_title); - builder.setMessage(R.string.action_forbidden_msg); - builder.setPositiveButton(R.string.authenticate_now_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - context.startActivity(new Intent(context, - FlattrAuthActivity.class)); - } - - } - ); - builder.setNegativeButton(R.string.visit_website_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, - uri)); - } - - } - ); - builder.create().show(); - } - - public static void showErrorDialog(final Context context, final String msg) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.error_label); - builder.setMessage(msg); - builder.setNeutralButton(android.R.string.ok, new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); - builder.create().show(); - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java b/app/src/main/java/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java deleted file mode 100644 index 296610871..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/flattr/SimpleFlattrThing.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.danoeh.antennapod.util.flattr; - -/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */ -public class SimpleFlattrThing implements FlattrThing { - public SimpleFlattrThing(String title, String url, FlattrStatus status) - { - this.title = title; - this.url = url; - this.status = status; - } - - public String getTitle() - { - return this.title; - } - - public String getPaymentLink() - { - return this.url; - } - - public FlattrStatus getFlattrStatus() - { - return this.status; - } - - private String title; - private String url; - private FlattrStatus status; -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java b/app/src/main/java/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java deleted file mode 100644 index b920559db..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/gui/FeedItemUndoToken.java +++ /dev/null @@ -1,55 +0,0 @@ -package de.danoeh.antennapod.util.gui; - -import android.os.Parcel; -import android.os.Parcelable; -import de.danoeh.antennapod.feed.FeedItem; - -/** - * Used by an UndoBarController for saving a removed FeedItem - */ -public class FeedItemUndoToken implements Parcelable { - private long itemId; - private long feedId; - private int position; - - public FeedItemUndoToken(FeedItem item, int position) { - this.itemId = item.getId(); - this.feedId = item.getFeed().getId(); - this.position = position; - } - - private FeedItemUndoToken(Parcel in) { - itemId = in.readLong(); - feedId = in.readLong(); - position = in.readInt(); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public FeedItemUndoToken createFromParcel(Parcel in) { - return new FeedItemUndoToken(in); - } - - public FeedItemUndoToken[] newArray(int size) { - return new FeedItemUndoToken[size]; - } - }; - - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel out, int flags) { - out.writeLong(itemId); - out.writeLong(feedId); - out.writeInt(position); - } - - public long getFeedItemId() { - return itemId; - } - - public int getPosition() { - return position; - } -} - diff --git a/app/src/main/java/de/danoeh/antennapod/util/id3reader/ChapterReader.java b/app/src/main/java/de/danoeh/antennapod/util/id3reader/ChapterReader.java deleted file mode 100644 index 257635129..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/id3reader/ChapterReader.java +++ /dev/null @@ -1,118 +0,0 @@ -package de.danoeh.antennapod.util.id3reader; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.ID3Chapter; -import de.danoeh.antennapod.util.id3reader.model.FrameHeader; -import de.danoeh.antennapod.util.id3reader.model.TagHeader; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.List; - -public class ChapterReader extends ID3Reader { - private static final String TAG = "ID3ChapterReader"; - - private static final String FRAME_ID_CHAPTER = "CHAP"; - private static final String FRAME_ID_TITLE = "TIT2"; - private static final String FRAME_ID_LINK = "WXXX"; - - private List chapters; - private ID3Chapter currentChapter; - - @Override - public int onStartTagHeader(TagHeader header) { - chapters = new ArrayList(); - System.out.println(header.toString()); - return ID3Reader.ACTION_DONT_SKIP; - } - - @Override - public int onStartFrameHeader(FrameHeader header, InputStream input) - throws IOException, ID3ReaderException { - System.out.println(header.toString()); - if (header.getId().equals(FRAME_ID_CHAPTER)) { - if (currentChapter != null) { - if (!hasId3Chapter(currentChapter)) { - chapters.add(currentChapter); - if (BuildConfig.DEBUG) Log.d(TAG, "Found chapter: " + currentChapter); - currentChapter = null; - } - } - StringBuffer elementId = new StringBuffer(); - readISOString(elementId, input, Integer.MAX_VALUE); - char[] startTimeSource = readBytes(input, 4); - long startTime = ((int) startTimeSource[0] << 24) - | ((int) startTimeSource[1] << 16) - | ((int) startTimeSource[2] << 8) | startTimeSource[3]; - currentChapter = new ID3Chapter(elementId.toString(), startTime); - skipBytes(input, 12); - return ID3Reader.ACTION_DONT_SKIP; - } else if (header.getId().equals(FRAME_ID_TITLE)) { - if (currentChapter != null && currentChapter.getTitle() == null) { - StringBuffer title = new StringBuffer(); - readString(title, input, header.getSize()); - currentChapter - .setTitle(title.toString()); - if (BuildConfig.DEBUG) Log.d(TAG, "Found title: " + currentChapter.getTitle()); - - return ID3Reader.ACTION_DONT_SKIP; - } - } else if (header.getId().equals(FRAME_ID_LINK)) { - if (currentChapter != null) { - // skip description - int descriptionLength = readString(null, input, header.getSize()); - StringBuffer link = new StringBuffer(); - readISOString(link, input, header.getSize() - descriptionLength); - String decodedLink = URLDecoder.decode(link.toString(), "UTF-8"); - - currentChapter.setLink(decodedLink); - - if (BuildConfig.DEBUG) Log.d(TAG, "Found link: " + currentChapter.getLink()); - return ID3Reader.ACTION_DONT_SKIP; - } - } else if (header.getId().equals("APIC")) { - Log.d(TAG, header.toString()); - } - - return super.onStartFrameHeader(header, input); - } - - private boolean hasId3Chapter(ID3Chapter chapter) { - for (Chapter c : chapters) { - if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) { - return true; - } - } - return false; - } - - @Override - public void onEndTag() { - if (currentChapter != null) { - if (!hasId3Chapter(currentChapter)) { - chapters.add(currentChapter); - } - } - System.out.println("Reached end of tag"); - if (chapters != null) { - for (Chapter c : chapters) { - System.out.println(c.toString()); - } - } - } - - @Override - public void onNoTagHeaderFound() { - System.out.println("No tag header found"); - super.onNoTagHeaderFound(); - } - - public List getChapters() { - return chapters; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3Reader.java b/app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3Reader.java deleted file mode 100644 index 252d64107..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3Reader.java +++ /dev/null @@ -1,250 +0,0 @@ -package de.danoeh.antennapod.util.id3reader; - -import de.danoeh.antennapod.util.id3reader.model.FrameHeader; -import de.danoeh.antennapod.util.id3reader.model.TagHeader; -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; - -/** - * Reads the ID3 Tag of a given file. In order to use this class, you should - * create a subclass of it and overwrite the onStart* - or onEnd* - methods. - */ -public class ID3Reader { - private static final int HEADER_LENGTH = 10; - private static final int ID3_LENGTH = 3; - private static final int FRAME_ID_LENGTH = 4; - - protected static final int ACTION_SKIP = 1; - protected static final int ACTION_DONT_SKIP = 2; - - protected int readerPosition; - - private static final byte ENCODING_UTF16_WITH_BOM = 1; - private static final byte ENCODING_UTF16_WITHOUT_BOM = 2; - private static final byte ENCODING_UTF8 = 3; - - private TagHeader tagHeader; - - public ID3Reader() { - } - - public final void readInputStream(InputStream input) throws IOException, - ID3ReaderException { - int rc; - readerPosition = 0; - char[] tagHeaderSource = readBytes(input, HEADER_LENGTH); - tagHeader = createTagHeader(tagHeaderSource); - if (tagHeader == null) { - onNoTagHeaderFound(); - } else { - rc = onStartTagHeader(tagHeader); - if (rc == ACTION_SKIP) { - onEndTag(); - } else { - while (readerPosition < tagHeader.getSize()) { - FrameHeader frameHeader = createFrameHeader(readBytes( - input, HEADER_LENGTH)); - if (checkForNullString(frameHeader.getId())) { - break; - } else { - rc = onStartFrameHeader(frameHeader, input); - if (rc == ACTION_SKIP) { - - if (frameHeader.getSize() + readerPosition > tagHeader - .getSize()) { - break; - } else { - skipBytes(input, frameHeader.getSize()); - } - } - } - } - onEndTag(); - } - } - } - - /** Returns true if string only contains null-bytes. */ - private boolean checkForNullString(String s) { - if (!s.isEmpty()) { - int i = 0; - if (s.charAt(i) == 0) { - for (i = 1; i < s.length(); i++) { - if (s.charAt(i) != 0) { - return false; - } - } - return true; - } - return false; - } else { - return true; - } - - } - - /** - * Read a certain number of bytes from the given input stream. This method - * changes the readerPosition-attribute. - */ - protected char[] readBytes(InputStream input, int number) - throws IOException, ID3ReaderException { - char[] header = new char[number]; - for (int i = 0; i < number; i++) { - int b = input.read(); - readerPosition++; - if (b != -1) { - header[i] = (char) b; - } else { - throw new ID3ReaderException("Unexpected end of stream"); - } - } - return header; - } - - /** - * Skip a certain number of bytes on the given input stream. This method - * changes the readerPosition-attribute. - */ - protected void skipBytes(InputStream input, int number) throws IOException { - if (number <= 0) { - number = 1; - } - IOUtils.skipFully(input, number); - - readerPosition += number; - } - - private TagHeader createTagHeader(char[] source) throws ID3ReaderException { - boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44) - && (source[2] == 0x33); - if (source.length != HEADER_LENGTH) { - throw new ID3ReaderException("Length of header must be " - + HEADER_LENGTH); - } - if (hasTag) { - String id = new String(source, 0, ID3_LENGTH); - char version = (char) ((source[3] << 8) | source[4]); - byte flags = (byte) source[5]; - int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8) - | source[9]; - size = unsynchsafe(size); - return new TagHeader(id, size, version, flags); - } else { - return null; - } - } - - private FrameHeader createFrameHeader(char[] source) - throws ID3ReaderException { - if (source.length != HEADER_LENGTH) { - throw new ID3ReaderException("Length of header must be " - + HEADER_LENGTH); - } - String id = new String(source, 0, FRAME_ID_LENGTH); - - int size = (((int) source[4]) << 24) | (((int) source[5]) << 16) - | (((int) source[6]) << 8) | source[7]; - if (tagHeader != null && tagHeader.getVersion() >= 0x0400) { - size = unsynchsafe(size); - } - char flags = (char) ((source[8] << 8) | source[9]); - return new FrameHeader(id, size, flags); - } - - private int unsynchsafe(int in) { - int out = 0; - int mask = 0x7F000000; - - while (mask != 0) { - out >>= 1; - out |= in & mask; - mask >>= 8; - } - - return out; - } - - protected int readString(StringBuffer buffer, InputStream input, int max) throws IOException, - ID3ReaderException { - if (max > 0) { - char[] encoding = readBytes(input, 1); - max--; - - if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) { - return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account - } else if (encoding[0] == ENCODING_UTF8) { - return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account - } else { - return readISOString(buffer, input, max) + 1; // take encoding byte into account - } - } else { - if (buffer != null) { - buffer.append(""); - } - return 0; - } - } - - protected int readISOString(StringBuffer buffer, InputStream input, int max) - throws IOException, ID3ReaderException { - - int bytesRead = 0; - char c; - while (++bytesRead <= max && (c = (char) input.read()) > 0) { - if (buffer != null) { - buffer.append(c); - } - } - return bytesRead; - } - - private int readUnicodeString(StringBuffer strBuffer, InputStream input, int max, Charset charset) - throws IOException, ID3ReaderException { - byte[] buffer = new byte[max]; - int c, cZero = -1; - int i = 0; - for (; i < max; i++) { - c = input.read(); - if (c == -1) { - break; - } else if (c == 0) { - if (cZero == 0) { - // termination character found - break; - } else { - cZero = 0; - } - } else { - buffer[i] = (byte) c; - cZero = -1; - } - } - if (strBuffer != null) { - strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString()); - } - return i; - } - - public int onStartTagHeader(TagHeader header) { - return ACTION_SKIP; - } - - public int onStartFrameHeader(FrameHeader header, InputStream input) - throws IOException, ID3ReaderException { - return ACTION_SKIP; - } - - public void onEndTag() { - - } - - public void onNoTagHeaderFound() { - - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java b/app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java deleted file mode 100644 index c458540ee..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/id3reader/ID3ReaderException.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.danoeh.antennapod.util.id3reader; - -public class ID3ReaderException extends Exception { - - public ID3ReaderException() { - } - - public ID3ReaderException(String arg0) { - super(arg0); - } - - public ID3ReaderException(Throwable arg0) { - super(arg0); - } - - public ID3ReaderException(String arg0, Throwable arg1) { - super(arg0, arg1); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java b/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java deleted file mode 100644 index df73393a5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/FrameHeader.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.danoeh.antennapod.util.id3reader.model; - -public class FrameHeader extends Header { - - protected char flags; - - public FrameHeader(String id, int size, char flags) { - super(id, size); - this.flags = flags; - } - - @Override - public String toString() { - return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size)); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/Header.java b/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/Header.java deleted file mode 100644 index 22d5b6376..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/Header.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.danoeh.antennapod.util.id3reader.model; - -public abstract class Header { - - protected String id; - protected int size; - - public Header(String id, int size) { - super(); - this.id = id; - this.size = size; - } - - public String getId() { - return id; - } - - public int getSize() { - return size; - } - - @Override - public String toString() { - return "Header [id=" + id + ", size=" + size + "]"; - } - - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/TagHeader.java b/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/TagHeader.java deleted file mode 100644 index ec99ef14e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/id3reader/model/TagHeader.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.danoeh.antennapod.util.id3reader.model; - -public class TagHeader extends Header { - - protected char version; - protected byte flags; - - public TagHeader(String id, int size, char version, byte flags) { - super(id, size); - this.version = version; - this.flags = flags; - } - - @Override - public String toString() { - return "TagHeader [version=" + version + ", flags=" + flags + ", id=" - + id + ", size=" + size + "]"; - } - - public char getVersion() { - return version; - } - - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java deleted file mode 100644 index 2c7a7f074..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java +++ /dev/null @@ -1,191 +0,0 @@ -package de.danoeh.antennapod.util.menuhandler; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.service.playback.PlaybackService; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.ShareUtils; - -/** - * Handles interactions with the FeedItemMenu. - */ -public class FeedItemMenuHandler { - private static final String TAG = "FeedItemMenuHandler"; - - private FeedItemMenuHandler() { - - } - - /** - * Used by the MenuHandler to access different types of menus through one - * interface - */ - public interface MenuInterface { - /** - * Implementations of this method should call findItem(id) on their - * menu-object and call setVisibility(visibility) on the returned - * MenuItem object. - */ - abstract void setItemVisibility(int id, boolean visible); - } - - /** - * This method should be called in the prepare-methods of menus. It changes - * the visibility of the menu items depending on a FeedItem's attributes. - * - * @param mi An instance of MenuInterface that the method uses to change a - * MenuItem's visibility - * @param selectedItem The FeedItem for which the menu is supposed to be prepared - * @param showExtendedMenu True if MenuItems that let the user share information about - * the FeedItem and visit its website should be set visible. This - * parameter should be set to false if the menu space is limited. - * @param queueAccess Used for testing if the queue contains the selected item - * @return Returns true if selectedItem is not null. - */ - public static boolean onPrepareMenu(MenuInterface mi, - FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { - if (selectedItem == null) { - return false; - } - DownloadRequester requester = DownloadRequester.getInstance(); - boolean hasMedia = selectedItem.getMedia() != null; - boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); - boolean downloading = hasMedia - && requester.isDownloadingFile(selectedItem.getMedia()); - boolean notLoadedAndNotLoading = hasMedia && (!downloaded) - && (!downloading); - boolean isPlaying = hasMedia - && selectedItem.getState() == FeedItem.State.PLAYING; - - FeedItem.State state = selectedItem.getState(); - - if (!isPlaying) { - mi.setItemVisibility(R.id.skip_episode_item, false); - } - if (!downloaded || isPlaying) { - mi.setItemVisibility(R.id.play_item, false); - mi.setItemVisibility(R.id.remove_item, false); - } - if (!notLoadedAndNotLoading) { - mi.setItemVisibility(R.id.download_item, false); - } - if (!(notLoadedAndNotLoading | downloading) | isPlaying) { - mi.setItemVisibility(R.id.stream_item, false); - } - if (!downloading) { - mi.setItemVisibility(R.id.cancel_download_item, false); - } - - boolean isInQueue = queueAccess.contains(selectedItem.getId()); - if (!isInQueue || isPlaying) { - mi.setItemVisibility(R.id.remove_from_queue_item, false); - } - if (!(!isInQueue && selectedItem.getMedia() != null)) { - mi.setItemVisibility(R.id.add_to_queue_item, false); - } - if (!showExtendedMenu || selectedItem.getLink() == null) { - mi.setItemVisibility(R.id.share_link_item, false); - } - - if (!BuildConfig.DEBUG - || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { - mi.setItemVisibility(R.id.mark_unread_item, false); - } - if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { - mi.setItemVisibility(R.id.mark_read_item, false); - } - - if (!showExtendedMenu || selectedItem.getLink() == null) { - mi.setItemVisibility(R.id.visit_website_item, false); - } - - if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) { - mi.setItemVisibility(R.id.support_item, false); - } - return true; - } - - /** - * The same method as onPrepareMenu(MenuInterface, FeedItem, boolean, QueueAccess), but lets the - * caller also specify a list of menu items that should not be shown. - * - * @param excludeIds Menu item that should be excluded - * @return true if selectedItem is not null. - */ - public static boolean onPrepareMenu(MenuInterface mi, - FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess, int... excludeIds) { - boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess); - if (rc && excludeIds != null) { - for (int id : excludeIds) { - mi.setItemVisibility(id, false); - } - } - - return rc; - } - - public static boolean onMenuItemClicked(Context context, int menuItemId, - FeedItem selectedItem) throws DownloadRequestException { - DownloadRequester requester = DownloadRequester.getInstance(); - switch (menuItemId) { - case R.id.skip_episode_item: - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); - break; - case R.id.download_item: - DBTasks.downloadFeedItems(context, selectedItem); - break; - case R.id.play_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - false); - break; - case R.id.remove_item: - DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); - break; - case R.id.cancel_download_item: - requester.cancelDownload(context, selectedItem.getMedia()); - break; - case R.id.mark_read_item: - DBWriter.markItemRead(context, selectedItem, true, true); - break; - case R.id.mark_unread_item: - DBWriter.markItemRead(context, selectedItem, false, true); - break; - case R.id.add_to_queue_item: - DBWriter.addQueueItem(context, selectedItem.getId()); - break; - case R.id.remove_from_queue_item: - DBWriter.removeQueueItem(context, selectedItem.getId(), true); - break; - case R.id.stream_item: - DBTasks.playMedia(context, selectedItem.getMedia(), true, true, - true); - break; - case R.id.visit_website_item: - Uri uri = Uri.parse(selectedItem.getLink()); - context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - break; - case R.id.support_item: - DBTasks.flattrItemIfLoggedIn(context, selectedItem); - break; - case R.id.share_link_item: - ShareUtils.shareFeedItemLink(context, selectedItem); - break; - default: - return false; - } - // Refresh menu state - - return true; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java deleted file mode 100644 index a3adec66d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java +++ /dev/null @@ -1,87 +0,0 @@ -package de.danoeh.antennapod.util.menuhandler; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.FeedInfoActivity; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.service.download.DownloadService; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.util.ShareUtils; - -/** Handles interactions with the FeedItemMenu. */ -public class FeedMenuHandler { - private static final String TAG = "FeedMenuHandler"; - - public static boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { - inflater.inflate(R.menu.feedlist, menu); - return true; - } - - public static boolean onPrepareOptionsMenu(Menu menu, Feed selectedFeed) { - if (selectedFeed == null) { - return true; - } - - if (BuildConfig.DEBUG) - Log.d(TAG, "Preparing options menu"); - menu.findItem(R.id.mark_all_read_item).setVisible( - selectedFeed.hasNewItems(true)); - if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) - menu.findItem(R.id.support_item).setVisible(true); - else - menu.findItem(R.id.support_item).setVisible(false); - MenuItem refresh = menu.findItem(R.id.refresh_item); - if (DownloadService.isRunning - && DownloadRequester.getInstance().isDownloadingFile( - selectedFeed)) { - refresh.setVisible(false); - } else { - refresh.setVisible(true); - } - - return true; - } - - /** - * NOTE: This method does not handle clicks on the 'remove feed' - item. - * - * @throws DownloadRequestException - */ - public static boolean onOptionsItemClicked(Context context, MenuItem item, - Feed selectedFeed) throws DownloadRequestException { - switch (item.getItemId()) { - case R.id.refresh_item: - DBTasks.refreshFeed(context, selectedFeed); - break; - case R.id.mark_all_read_item: - DBWriter.markFeedRead(context, selectedFeed.getId()); - break; - case R.id.visit_website_item: - Uri uri = Uri.parse(selectedFeed.getLink()); - context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - break; - case R.id.support_item: - DBTasks.flattrFeedIfLoggedIn(context, selectedFeed); - break; - case R.id.share_link_item: - ShareUtils.shareFeedlink(context, selectedFeed); - break; - case R.id.share_source_item: - ShareUtils.shareFeedDownloadLink(context, selectedFeed); - break; - default: - return false; - } - return true; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java deleted file mode 100644 index 7aa04d24c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/MenuItemUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.danoeh.antennapod.util.menuhandler; - -import android.support.v4.view.MenuItemCompat; -import android.support.v7.widget.SearchView; -import android.view.Menu; -import android.view.MenuItem; - -import de.danoeh.antennapod.R; - -/** - * Utilities for menu items - */ -public class MenuItemUtils { - - public static MenuItem addSearchItem(Menu menu, SearchView searchView) { - MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); - MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); - MenuItemCompat.setActionView(item, searchView); - return item; - } - - /** - * Checks if the navigation drawer of the DrawerActivity is opened. This can be useful for Fragments - * that hide their menu if the navigation drawer is open. - * - * @return True if the drawer is open, false otherwise (also if the parameter is null) - */ - public static boolean isActivityDrawerOpen(NavDrawerActivity activity) { - return activity != null && activity.isDrawerOpen(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java b/app/src/main/java/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java deleted file mode 100644 index 9c611a452..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/menuhandler/NavDrawerActivity.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.danoeh.antennapod.util.menuhandler; - -/** - * Defines useful methods for activities that have a navigation drawer - */ -public interface NavDrawerActivity { - - public boolean isDrawerOpen(); -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/AudioPlayer.java b/app/src/main/java/de/danoeh/antennapod/util/playback/AudioPlayer.java deleted file mode 100644 index bd49b0d18..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/AudioPlayer.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.content.Context; -import android.util.Log; -import android.view.SurfaceHolder; -import com.aocate.media.MediaPlayer; - -public class AudioPlayer extends MediaPlayer implements IPlayer { - private static final String TAG = "AudioPlayer"; - - public AudioPlayer(Context context) { - super(context); - } - - @Override - public void setScreenOnWhilePlaying(boolean screenOn) { - Log.e(TAG, "Setting screen on while playing not supported in Audio Player"); - throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player"); - - } - - @Override - public void setDisplay(SurfaceHolder sh) { - if (sh != null) { - Log.e(TAG, "Setting display not supported in Audio Player"); - throw new UnsupportedOperationException("Setting display not supported in Audio Player"); - } - } - - @Override - public void setVideoScalingMode(int mode) { - throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/ExternalMedia.java b/app/src/main/java/de/danoeh/antennapod/util/playback/ExternalMedia.java deleted file mode 100644 index 3f6e6ae0a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/ExternalMedia.java +++ /dev/null @@ -1,237 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.util.ChapterUtils; - -import java.io.File; -import java.io.InputStream; -import java.util.List; -import java.util.concurrent.Callable; - -/** Represents a media file that is stored on the local storage device. */ -public class ExternalMedia implements Playable { - - public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2; - public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl"; - public static final String PREF_POSITION = "ExternalMedia.PrefPosition"; - public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType"; - - private String source; - - private String episodeTitle; - private String feedTitle; - private MediaType mediaType = MediaType.AUDIO; - private List chapters; - private int duration; - private int position; - - public ExternalMedia(String source, MediaType mediaType) { - super(); - this.source = source; - this.mediaType = mediaType; - } - - public ExternalMedia(String source, MediaType mediaType, int position) { - this(source, mediaType); - this.position = position; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(source); - dest.writeString(mediaType.toString()); - dest.writeInt(position); - } - - @Override - public void writeToPreferences(Editor prefEditor) { - prefEditor.putString(PREF_SOURCE_URL, source); - prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString()); - prefEditor.putInt(PREF_POSITION, position); - } - - @Override - public void loadMetadata() throws PlayableException { - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); - try { - mmr.setDataSource(source); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - throw new PlayableException( - "IllegalArgumentException when setting up MediaMetadataReceiver"); - } catch (RuntimeException e) { - // http://code.google.com/p/android/issues/detail?id=39770 - e.printStackTrace(); - throw new PlayableException( - "RuntimeException when setting up MediaMetadataRetriever"); - } - episodeTitle = mmr - .extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); - feedTitle = mmr - .extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); - try { - duration = Integer.parseInt(mmr - .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); - } catch (NumberFormatException e) { - e.printStackTrace(); - throw new PlayableException("NumberFormatException when reading duration of media file"); - } - ChapterUtils.loadChaptersFromFileUrl(this); - } - - @Override - public void loadChapterMarks() { - - } - - @Override - public String getEpisodeTitle() { - return episodeTitle; - } - - @Override - public Callable loadShownotes() { - return new Callable() { - @Override - public String call() throws Exception { - return ""; - } - }; - } - - @Override - public List getChapters() { - return chapters; - } - - @Override - public String getWebsiteLink() { - return null; - } - - @Override - public String getPaymentLink() { - return null; - } - - @Override - public String getFeedTitle() { - return feedTitle; - } - - @Override - public Object getIdentifier() { - return source; - } - - @Override - public int getDuration() { - return duration; - } - - @Override - public int getPosition() { - return position; - } - - @Override - public MediaType getMediaType() { - return mediaType; - } - - @Override - public String getLocalMediaUrl() { - return source; - } - - @Override - public String getStreamUrl() { - return null; - } - - @Override - public boolean localFileAvailable() { - return true; - } - - @Override - public boolean streamAvailable() { - return false; - } - - @Override - public void saveCurrentPosition(SharedPreferences pref, int newPosition) { - SharedPreferences.Editor editor = pref.edit(); - editor.putInt(PREF_POSITION, newPosition); - position = newPosition; - editor.commit(); - } - - @Override - public void setPosition(int newPosition) { - position = newPosition; - } - - @Override - public void setDuration(int newDuration) { - duration = newDuration; - } - - @Override - public void onPlaybackStart() { - - } - - @Override - public void onPlaybackCompleted() { - - } - - @Override - public int getPlayableType() { - return PLAYABLE_TYPE_EXTERNAL_MEDIA; - } - - @Override - public void setChapters(List chapters) { - this.chapters = chapters; - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public ExternalMedia createFromParcel(Parcel in) { - String source = in.readString(); - MediaType type = MediaType.valueOf(in.readString()); - int position = 0; - if (in.dataAvail() > 0) { - position = in.readInt(); - } - ExternalMedia extMedia = new ExternalMedia(source, type, position); - return extMedia; - } - - public ExternalMedia[] newArray(int size) { - return new ExternalMedia[size]; - } - }; - - @Override - public Uri getImageUri() { - if (localFileAvailable()) { - return new Uri.Builder().scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()).build(); - } else { - return null; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/IPlayer.java b/app/src/main/java/de/danoeh/antennapod/util/playback/IPlayer.java deleted file mode 100644 index 2d4551b13..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/IPlayer.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.content.Context; -import android.view.SurfaceHolder; - -import java.io.IOException; - -public interface IPlayer { - boolean canSetPitch(); - - boolean canSetSpeed(); - - float getCurrentPitchStepsAdjustment(); - - int getCurrentPosition(); - - float getCurrentSpeedMultiplier(); - - int getDuration(); - - float getMaxSpeedMultiplier(); - - float getMinSpeedMultiplier(); - - boolean isLooping(); - - boolean isPlaying(); - - void pause(); - - void prepare() throws IllegalStateException, IOException; - - void prepareAsync(); - - void release(); - - void reset(); - - void seekTo(int msec); - - void setAudioStreamType(int streamtype); - - void setScreenOnWhilePlaying(boolean screenOn); - - void setDataSource(String path) throws IllegalStateException, IOException, - IllegalArgumentException, SecurityException; - - void setDisplay(SurfaceHolder sh); - - void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); - - void setLooping(boolean looping); - - void setPitchStepsAdjustment(float pitchSteps); - - void setPlaybackPitch(float f); - - void setPlaybackSpeed(float f); - - void setVolume(float left, float right); - - void start(); - - void stop(); - - public void setVideoScalingMode(int mode); - - public void setWakeMode(Context context, int mode); -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/MediaPlayerError.java b/app/src/main/java/de/danoeh/antennapod/util/playback/MediaPlayerError.java deleted file mode 100644 index 23ead581f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/MediaPlayerError.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.content.Context; -import android.media.MediaPlayer; -import de.danoeh.antennapod.R; - -/** Utility class for MediaPlayer errors. */ -public class MediaPlayerError { - - /** Get a human-readable string for a specific error code. */ - public static String getErrorString(Context context, int code) { - int resId; - switch(code) { - case MediaPlayer.MEDIA_ERROR_SERVER_DIED: - resId = R.string.playback_error_server_died; - break; - default: - resId = R.string.playback_error_unknown; - break; - } - return context.getString(resId); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/Playable.java b/app/src/main/java/de/danoeh/antennapod/util/playback/Playable.java deleted file mode 100644 index 004ae56bb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/Playable.java +++ /dev/null @@ -1,207 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Parcelable; -import android.util.Log; - -import java.util.List; - -import de.danoeh.antennapod.asynctask.PicassoImageResource; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.util.ShownotesProvider; - -/** - * Interface for objects that can be played by the PlaybackService. - */ -public interface Playable extends Parcelable, - ShownotesProvider, PicassoImageResource { - - /** - * Save information about the playable in a preference so that it can be - * restored later via PlayableUtils.createInstanceFromPreferences. - * Implementations must NOT call commit() after they have written the values - * to the preferences file. - */ - public void writeToPreferences(SharedPreferences.Editor prefEditor); - - /** - * This method is called from a separate thread by the PlaybackService. - * Playable objects should load their metadata in this method. This method - * should execute as quickly as possible and NOT load chapter marks if no - * local file is available. - */ - public void loadMetadata() throws PlayableException; - - /** - * This method is called from a separate thread by the PlaybackService. - * Playable objects should load their chapter marks in this method if no - * local file was available when loadMetadata() was called. - */ - public void loadChapterMarks(); - - /** - * Returns the title of the episode that this playable represents - */ - public String getEpisodeTitle(); - - /** - * Returns a list of chapter marks or null if this Playable has no chapters. - */ - public List getChapters(); - - /** - * Returns a link to a website that is meant to be shown in a browser - */ - public String getWebsiteLink(); - - public String getPaymentLink(); - - /** - * Returns the title of the feed this Playable belongs to. - */ - public String getFeedTitle(); - - /** - * Returns a unique identifier, for example a file url or an ID from a - * database. - */ - public Object getIdentifier(); - - /** - * Return duration of object or 0 if duration is unknown. - */ - public int getDuration(); - - /** - * Return position of object or 0 if position is unknown. - */ - public int getPosition(); - - /** - * Returns the type of media. This method should return the correct value - * BEFORE loadMetadata() is called. - */ - public MediaType getMediaType(); - - /** - * Returns an url to a local file that can be played or null if this file - * does not exist. - */ - public String getLocalMediaUrl(); - - /** - * Returns an url to a file that can be streamed by the player or null if - * this url is not known. - */ - public String getStreamUrl(); - - /** - * Returns true if a local file that can be played is available. getFileUrl - * MUST return a non-null string if this method returns true. - */ - public boolean localFileAvailable(); - - /** - * Returns true if a streamable file is available. getStreamUrl MUST return - * a non-null string if this method returns true. - */ - public boolean streamAvailable(); - - /** - * Saves the current position of this object. Implementations can use the - * provided SharedPreference to save this information and retrieve it later - * via PlayableUtils.createInstanceFromPreferences. - */ - public void saveCurrentPosition(SharedPreferences pref, int newPosition); - - public void setPosition(int newPosition); - - public void setDuration(int newDuration); - - /** - * Is called by the PlaybackService when playback starts. - */ - public void onPlaybackStart(); - - /** - * Is called by the PlaybackService when playback is completed. - */ - public void onPlaybackCompleted(); - - /** - * Returns an integer that must be unique among all Playable classes. The - * return value is later used by PlayableUtils to determine the type of the - * Playable object that is restored. - */ - public int getPlayableType(); - - public void setChapters(List chapters); - - /** - * Provides utility methods for Playable objects. - */ - public static class PlayableUtils { - private static final String TAG = "PlayableUtils"; - - /** - * Restores a playable object from a sharedPreferences file. This method might load data from the database, - * depending on the type of playable that was restored. - * - * @param type An integer that represents the type of the Playable object - * that is restored. - * @param pref The SharedPreferences file from which the Playable object - * is restored - * @return The restored Playable object - */ - public static Playable createInstanceFromPreferences(Context context, int type, - SharedPreferences pref) { - // ADD new Playable types here: - switch (type) { - case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: - long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); - if (mediaId != -1) { - return DBReader.getFeedMedia(context, mediaId); - } - break; - case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: - String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, - null); - String mediaType = pref.getString( - ExternalMedia.PREF_MEDIA_TYPE, null); - if (source != null && mediaType != null) { - int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); - return new ExternalMedia(source, - MediaType.valueOf(mediaType), position); - } - break; - } - Log.e(TAG, "Could not restore Playable object from preferences"); - return null; - } - } - - public static class PlayableException extends Exception { - private static final long serialVersionUID = 1L; - - public PlayableException() { - super(); - } - - public PlayableException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public PlayableException(String detailMessage) { - super(detailMessage); - } - - public PlayableException(Throwable throwable) { - super(throwable); - } - - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/PlaybackController.java b/app/src/main/java/de/danoeh/antennapod/util/playback/PlaybackController.java deleted file mode 100644 index 64dbf4868..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/PlaybackController.java +++ /dev/null @@ -1,784 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.app.Activity; -import android.content.*; -import android.content.res.TypedArray; -import android.media.MediaPlayer; -import android.os.AsyncTask; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.util.Log; -import android.util.Pair; -import android.view.SurfaceHolder; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ImageButton; -import android.widget.SeekBar; -import android.widget.TextView; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.playback.PlaybackService; -import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.service.playback.PlayerStatus; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.playback.Playable.PlayableUtils; - -import java.util.concurrent.*; - -/** - * Communicates with the playback service. GUI classes should use this class to - * control playback instead of communicating with the PlaybackService directly. - */ -public abstract class PlaybackController { - private static final String TAG = "PlaybackController"; - - public static final int INVALID_TIME = -1; - - private final Activity activity; - - private PlaybackService playbackService; - private Playable media; - private PlayerStatus status; - - private ScheduledThreadPoolExecutor schedExecutor; - private static final int SCHED_EX_POOLSIZE = 1; - - protected MediaPositionObserver positionObserver; - protected ScheduledFuture positionObserverFuture; - - private boolean mediaInfoLoaded = false; - private boolean released = false; - - /** - * True if controller should reinit playback service if 'pause' button is - * pressed. - */ - private boolean reinitOnPause; - - public PlaybackController(Activity activity, boolean reinitOnPause) { - Validate.notNull(activity); - - this.activity = activity; - this.reinitOnPause = reinitOnPause; - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, - "Rejected execution of runnable in schedExecutor"); - } - } - ); - } - - /** - * Creates a new connection to the playbackService. Should be called in the - * activity's onResume() method. - */ - public void init() { - activity.registerReceiver(statusUpdate, new IntentFilter( - PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); - - activity.registerReceiver(notificationReceiver, new IntentFilter( - PlaybackService.ACTION_PLAYER_NOTIFICATION)); - - activity.registerReceiver(shutdownReceiver, new IntentFilter( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - - if (!released) { - bindToService(); - } else { - throw new IllegalStateException( - "Can't call init() after release() has been called"); - } - } - - /** - * Should be called if the PlaybackController is no longer needed, for - * example in the activity's onStop() method. - */ - public void release() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Releasing PlaybackController"); - - try { - activity.unregisterReceiver(statusUpdate); - } catch (IllegalArgumentException e) { - // ignore - } - - try { - activity.unregisterReceiver(notificationReceiver); - } catch (IllegalArgumentException e) { - // ignore - } - - try { - activity.unbindService(mConnection); - } catch (IllegalArgumentException e) { - // ignore - } - - try { - activity.unregisterReceiver(shutdownReceiver); - } catch (IllegalArgumentException e) { - // ignore - } - cancelPositionObserver(); - schedExecutor.shutdownNow(); - media = null; - released = true; - - } - - /** - * Should be called in the activity's onPause() method. - */ - public void pause() { - mediaInfoLoaded = false; - } - - /** - * Tries to establish a connection to the PlaybackService. If it isn't - * running, the PlaybackService will be started with the last played media - * as the arguments of the launch intent. - */ - private void bindToService() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Trying to connect to service"); - AsyncTask intentLoader = new AsyncTask() { - @Override - protected Intent doInBackground(Void... voids) { - return getPlayLastPlayedMediaIntent(); - } - - @Override - protected void onPostExecute(Intent serviceIntent) { - boolean bound = false; - if (!PlaybackService.started) { - if (serviceIntent != null) { - if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service"); - activity.startService(serviceIntent); - bound = activity.bindService(serviceIntent, mConnection, 0); - } else { - status = PlayerStatus.STOPPED; - setupGUI(); - handleStatus(); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "PlaybackService is running, trying to connect without start command."); - bound = activity.bindService(new Intent(activity, - PlaybackService.class), mConnection, 0); - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Result for service binding: " + bound); - } - }; - intentLoader.execute(); - } - - /** - * Returns an intent that starts the PlaybackService and plays the last - * played media or null if no last played media could be found. - */ - private Intent getPlayLastPlayedMediaIntent() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Trying to restore last played media"); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(activity.getApplicationContext()); - long currentlyPlayingMedia = PlaybackPreferences - .getCurrentlyPlayingMedia(); - if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { - Playable media = PlayableUtils.createInstanceFromPreferences(activity, - (int) currentlyPlayingMedia, prefs); - if (media != null) { - Intent serviceIntent = new Intent(activity, - PlaybackService.class); - serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - serviceIntent.putExtra( - PlaybackService.EXTRA_START_WHEN_PREPARED, false); - serviceIntent.putExtra( - PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false); - boolean fileExists = media.localFileAvailable(); - boolean lastIsStream = PlaybackPreferences - .getCurrentEpisodeIsStream(); - if (!fileExists && !lastIsStream && media instanceof FeedMedia) { - DBTasks.notifyMissingFeedMediaFile( - activity, (FeedMedia) media); - } - serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, - lastIsStream || !fileExists); - return serviceIntent; - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "No last played media found"); - return null; - } - - public abstract void setupGUI(); - - private void setupPositionObserver() { - if ((positionObserverFuture != null && positionObserverFuture - .isCancelled()) - || (positionObserverFuture != null && positionObserverFuture - .isDone()) || positionObserverFuture == null) { - - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting up position observer"); - positionObserver = new MediaPositionObserver(); - positionObserverFuture = schedExecutor.scheduleWithFixedDelay( - positionObserver, MediaPositionObserver.WAITING_INTERVALL, - MediaPositionObserver.WAITING_INTERVALL, - TimeUnit.MILLISECONDS); - } - } - - private void cancelPositionObserver() { - if (positionObserverFuture != null) { - boolean result = positionObserverFuture.cancel(true); - if (BuildConfig.DEBUG) - Log.d(TAG, "PositionObserver cancelled. Result: " + result); - } - } - - public abstract void onPositionObserverUpdate(); - - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - playbackService = ((PlaybackService.LocalBinder) service) - .getService(); - if (!released) { - queryService(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Connection to Service established"); - } else { - Log.i(TAG, "Connection to playback service has been established, but controller has already been released"); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - playbackService = null; - if (BuildConfig.DEBUG) - Log.d(TAG, "Disconnected from Service"); - - } - }; - - protected BroadcastReceiver statusUpdate = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received statusUpdate Intent."); - if (isConnectedToPlaybackService()) { - PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); - status = info.playerStatus; - media = info.playable; - handleStatus(); - } else { - Log.w(TAG, - "Couldn't receive status update: playbackService was null"); - bindToService(); - } - } - }; - - protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (isConnectedToPlaybackService()) { - int type = intent.getIntExtra( - PlaybackService.EXTRA_NOTIFICATION_TYPE, -1); - int code = intent.getIntExtra( - PlaybackService.EXTRA_NOTIFICATION_CODE, -1); - if (code != -1 && type != -1) { - switch (type) { - case PlaybackService.NOTIFICATION_TYPE_ERROR: - handleError(code); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE: - float progress = ((float) code) / 100; - onBufferUpdate(progress); - break; - case PlaybackService.NOTIFICATION_TYPE_RELOAD: - cancelPositionObserver(); - mediaInfoLoaded = false; - queryService(); - onReloadNotification(intent.getIntExtra( - PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); - break; - case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: - onSleepTimerUpdate(); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_START: - onBufferStart(); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_END: - onBufferEnd(); - break; - case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END: - onPlaybackEnd(); - break; - case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE: - onPlaybackSpeedChange(); - break; - } - - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Bad arguments. Won't handle intent"); - } - } else { - bindToService(); - } - } - - }; - - private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (isConnectedToPlaybackService()) { - if (StringUtils.equals(intent.getAction(), - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - release(); - onShutdownNotification(); - } - } - } - }; - - public abstract void onPlaybackSpeedChange(); - - public abstract void onShutdownNotification(); - - /** - * Called when the currently displayed information should be refreshed. - */ - public abstract void onReloadNotification(int code); - - public abstract void onBufferStart(); - - public abstract void onBufferEnd(); - - public abstract void onBufferUpdate(float progress); - - public abstract void onSleepTimerUpdate(); - - public abstract void handleError(int code); - - public abstract void onPlaybackEnd(); - - /** - * Is called whenever the PlaybackService changes it's status. This method - * should be used to update the GUI or start/cancel background threads. - */ - private void handleStatus() { - final int playResource; - final int pauseResource; - final CharSequence playText = activity.getString(R.string.play_label); - final CharSequence pauseText = activity.getString(R.string.pause_label); - - if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) { - TypedArray res = activity.obtainStyledAttributes(new int[]{ - R.attr.av_play, R.attr.av_pause}); - playResource = res.getResourceId(0, R.drawable.av_play); - pauseResource = res.getResourceId(1, R.drawable.av_pause); - res.recycle(); - } else { - playResource = R.drawable.ic_action_play_over_video; - pauseResource = R.drawable.ic_action_pause_over_video; - } - - switch (status) { - - case ERROR: - postStatusMsg(R.string.player_error_msg); - handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); - break; - case PAUSED: - clearStatusMsg(); - checkMediaInfoLoaded(); - cancelPositionObserver(); - updatePlayButtonAppearance(playResource, playText); - if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { - setScreenOn(false); - } - break; - case PLAYING: - clearStatusMsg(); - checkMediaInfoLoaded(); - if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { - onAwaitingVideoSurface(); - setScreenOn(true); - } - setupPositionObserver(); - updatePlayButtonAppearance(pauseResource, pauseText); - break; - case PREPARING: - postStatusMsg(R.string.player_preparing_msg); - checkMediaInfoLoaded(); - if (playbackService != null) { - if (playbackService.isStartWhenPrepared()) { - updatePlayButtonAppearance(pauseResource, pauseText); - } else { - updatePlayButtonAppearance(playResource, playText); - } - } - break; - case STOPPED: - postStatusMsg(R.string.player_stopped_msg); - break; - case PREPARED: - checkMediaInfoLoaded(); - postStatusMsg(R.string.player_ready_msg); - updatePlayButtonAppearance(playResource, playText); - break; - case SEEKING: - postStatusMsg(R.string.player_seeking_msg); - break; - case INITIALIZED: - checkMediaInfoLoaded(); - clearStatusMsg(); - updatePlayButtonAppearance(playResource, playText); - break; - } - } - - private void checkMediaInfoLoaded() { - mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo()); - } - - private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { - ImageButton butPlay = getPlayButton(); - butPlay.setImageResource(resource); - butPlay.setContentDescription(contentDescription); - } - - public abstract ImageButton getPlayButton(); - - public abstract void postStatusMsg(int msg); - - public abstract void clearStatusMsg(); - - public abstract boolean loadMediaInfo(); - - public abstract void onAwaitingVideoSurface(); - - /** - * Called when connection to playback service has been established or - * information has to be refreshed - */ - void queryService() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Querying service info"); - if (playbackService != null) { - status = playbackService.getStatus(); - media = playbackService.getPlayable(); - /* - if (media == null) { - Log.w(TAG, - "PlaybackService has no media object. Trying to restore last played media."); - Intent serviceIntent = getPlayLastPlayedMediaIntent(); - if (serviceIntent != null) { - activity.startService(serviceIntent); - } - } - */ - onServiceQueried(); - - setupGUI(); - handleStatus(); - // make sure that new media is loaded if it's available - mediaInfoLoaded = false; - - } else { - Log.e(TAG, - "queryService() was called without an existing connection to playbackservice"); - } - } - - public abstract void onServiceQueried(); - - /** - * Should be used by classes which implement the OnSeekBarChanged interface. - */ - public float onSeekBarProgressChanged(SeekBar seekBar, int progress, - boolean fromUser, TextView txtvPosition) { - if (fromUser && playbackService != null && media != null) { - float prog = progress / ((float) seekBar.getMax()); - int duration = media.getDuration(); - txtvPosition.setText(Converter - .getDurationStringLong((int) (prog * duration))); - return prog; - } - return 0; - - } - - /** - * Should be used by classes which implement the OnSeekBarChanged interface. - */ - public void onSeekBarStartTrackingTouch(SeekBar seekBar) { - // interrupt position Observer, restart later - cancelPositionObserver(); - } - - /** - * Should be used by classes which implement the OnSeekBarChanged interface. - */ - public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { - if (playbackService != null) { - playbackService.seekTo((int) (prog * media.getDuration())); - setupPositionObserver(); - } - } - - /** - * Should be implemented by classes that show a video. The default implementation - * does nothing - * - * @param enable True if the screen should be kept on, false otherwise - */ - protected void setScreenOn(boolean enable) { - - } - - public OnClickListener newOnPlayButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (playbackService != null) { - switch (status) { - case PLAYING: - playbackService.pause(true, reinitOnPause); - break; - case PAUSED: - case PREPARED: - playbackService.resume(); - break; - case PREPARING: - playbackService.setStartWhenPrepared(!playbackService - .isStartWhenPrepared()); - if (reinitOnPause - && playbackService.isStartWhenPrepared() == false) { - playbackService.reinit(); - } - break; - case INITIALIZED: - playbackService.setStartWhenPrepared(true); - playbackService.prepare(); - break; - } - } else { - Log.w(TAG, - "Play/Pause button was pressed, but playbackservice was null!"); - } - } - - }; - } - - public OnClickListener newOnRevButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(-UserPreferences.getSeekDeltaMs()); - } - } - }; - } - - public OnClickListener newOnFFButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(UserPreferences.getSeekDeltaMs()); - } - } - }; - } - - public boolean serviceAvailable() { - return playbackService != null; - } - - public int getPosition() { - if (playbackService != null) { - return playbackService.getCurrentPosition(); - } else { - return PlaybackService.INVALID_TIME; - } - } - - public int getDuration() { - if (playbackService != null) { - return playbackService.getDuration(); - } else { - return PlaybackService.INVALID_TIME; - } - } - - public Playable getMedia() { - return media; - } - - public boolean sleepTimerActive() { - return playbackService != null && playbackService.sleepTimerActive(); - } - - public boolean sleepTimerNotActive() { - return playbackService != null && !playbackService.sleepTimerActive(); - } - - public void disableSleepTimer() { - if (playbackService != null) { - playbackService.disableSleepTimer(); - } - } - - public long getSleepTimerTimeLeft() { - if (playbackService != null) { - return playbackService.getSleepTimerTimeLeft(); - } else { - return INVALID_TIME; - } - } - - public void setSleepTimer(long time) { - if (playbackService != null) { - playbackService.setSleepTimer(time); - } - } - - public void seekToChapter(Chapter chapter) { - if (playbackService != null) { - playbackService.seekToChapter(chapter); - } - } - - public void seekTo(int time) { - if (playbackService != null) { - playbackService.seekTo(time); - } - } - - public void setVideoSurface(SurfaceHolder holder) { - if (playbackService != null) { - playbackService.setVideoSurface(holder); - } - } - - public PlayerStatus getStatus() { - return status; - } - - public boolean canSetPlaybackSpeed() { - return playbackService != null && playbackService.canSetSpeed(); - } - - public void setPlaybackSpeed(float speed) { - if (playbackService != null) { - playbackService.setSpeed(speed); - } - } - - public float getCurrentPlaybackSpeedMultiplier() { - if (canSetPlaybackSpeed()) { - return playbackService.getCurrentPlaybackSpeed(); - } else { - return -1; - } - } - - public boolean isPlayingVideo() { - if (playbackService != null) { - return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; - } - return false; - } - - public Pair getVideoSize() { - if (playbackService != null) { - return playbackService.getVideoSize(); - } else { - return null; - } - } - - - /** - * Returns true if PlaybackController can communicate with the playback - * service. - */ - public boolean isConnectedToPlaybackService() { - return playbackService != null; - } - - public void notifyVideoSurfaceAbandoned() { - if (playbackService != null) { - playbackService.notifyVideoSurfaceAbandoned(); - } - } - - /** - * Move service into INITIALIZED state if it's paused to save bandwidth - */ - public void reinitServiceIfPaused() { - if (playbackService != null - && playbackService.isStreaming() - && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService - .getStatus() == PlayerStatus.PREPARING && playbackService - .isStartWhenPrepared() == false))) { - playbackService.reinit(); - } - } - - /** - * Refreshes the current position of the media file that is playing. - */ - public class MediaPositionObserver implements Runnable { - - public static final int WAITING_INTERVALL = 1000; - - @Override - public void run() { - if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) { - activity.runOnUiThread(new Runnable() { - - @Override - public void run() { - onPositionObserverUpdate(); - } - }); - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/Timeline.java b/app/src/main/java/de/danoeh/antennapod/util/playback/Timeline.java deleted file mode 100644 index ceed68183..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/Timeline.java +++ /dev/null @@ -1,161 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.Log; -import android.util.TypedValue; - -import org.apache.commons.lang3.Validate; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.ShownotesProvider; - -/** - * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the - * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's - * shownotes. - *

- * A timeline object needs a shownotesProvider from which the chapter information is retrieved and shownotes are generated. - */ -public class Timeline { - private static final String TAG = "Timeline"; - - private static final String WEBVIEW_STYLE = "@font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }"; - - - private ShownotesProvider shownotesProvider; - - - private final String colorString; - private final int pageMargin; - - public Timeline(Context context, ShownotesProvider shownotesProvider) { - if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null"); - this.shownotesProvider = shownotesProvider; - - TypedArray res = context - .getTheme() - .obtainStyledAttributes( - new int[]{android.R.attr.textColorPrimary}); - int colorResource = res.getColor(0, 0); - colorString = String.format("#%06X", - 0xFFFFFF & colorResource); - res.recycle(); - - pageMargin = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 8, context.getResources() - .getDisplayMetrics() - ); - } - - private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))"); - private static final String TIMECODE_LINK = "%s"; - private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b"); - - /** - * Applies an app-specific CSS stylesheet and adds timecode links (optional). - *

- * This method does NOT change the original shownotes string of the shownotesProvider object and it should - * also not be changed by the caller. - * - * @param addTimecodes True if this method should add timecode links - * @return The processed HTML string. - */ - public String processShownotes(final boolean addTimecodes) { - final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null; - - // load shownotes - - String shownotes; - try { - shownotes = shownotesProvider.loadShownotes().call(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - if (shownotes == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string"); - return ""; - } - - Document document = Jsoup.parse(shownotes); - - // apply style - String styleStr = String.format(WEBVIEW_STYLE, colorString, "100%", pageMargin, - pageMargin, pageMargin, pageMargin); - document.head().appendElement("style").attr("type", "text/css").text(styleStr); - - // apply timecode links - if (addTimecodes) { - Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); - for (Element element : elementsWithTimeCodes) { - Matcher matcherLong = TIMECODE_REGEX.matcher(element.text()); - StringBuffer buffer = new StringBuffer(); - while (matcherLong.find()) { - String h = matcherLong.group(1); - String group = matcherLong.group(0); - int time = (h != null) ? Converter.durationStringLongToMs(group) : - Converter.durationStringShortToMs(group); - - String rep; - if (playable == null || playable.getDuration() > time) { - rep = String.format(TIMECODE_LINK, time, group); - } else { - rep = group; - } - matcherLong.appendReplacement(buffer, rep); - } - matcherLong.appendTail(buffer); - - element.html(buffer.toString()); - } - } - - Log.i(TAG, "Out: " + document.toString()); - return document.toString(); - } - - - /** - * Returns true if the given link is a timecode link. - */ - public static boolean isTimecodeLink(String link) { - return link != null && link.matches(TIMECODE_LINK_REGEX.pattern()); - } - - /** - * Returns the time in milliseconds that is attached to this link or -1 - * if the link is no valid timecode link. - */ - public static int getTimecodeLinkTime(String link) { - if (isTimecodeLink(link)) { - Matcher m = TIMECODE_LINK_REGEX.matcher(link); - - try { - if (m.find()) { - return Integer.valueOf(m.group(1)); - } - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - return -1; - } - - - public void setShownotesProvider(ShownotesProvider shownotesProvider) { - Validate.notNull(shownotesProvider); - this.shownotesProvider = shownotesProvider; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/playback/VideoPlayer.java b/app/src/main/java/de/danoeh/antennapod/util/playback/VideoPlayer.java deleted file mode 100644 index ea9c692ab..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/playback/VideoPlayer.java +++ /dev/null @@ -1,67 +0,0 @@ -package de.danoeh.antennapod.util.playback; - -import android.media.MediaPlayer; -import android.util.Log; - -public class VideoPlayer extends MediaPlayer implements IPlayer { - private static final String TAG = "VideoPlayer"; - - @Override - public boolean canSetPitch() { - return false; - } - - @Override - public boolean canSetSpeed() { - return false; - } - - @Override - public float getCurrentPitchStepsAdjustment() { - return 1; - } - - @Override - public float getCurrentSpeedMultiplier() { - return 1; - } - - @Override - public float getMaxSpeedMultiplier() { - return 1; - } - - @Override - public float getMinSpeedMultiplier() { - return 1; - } - - @Override - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException { - Log.e(TAG, "Setting enable speed adjustment unsupported in video player"); - throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player"); - } - - @Override - public void setPitchStepsAdjustment(float pitchSteps) { - Log.e(TAG, "Setting pitch steps adjustment unsupported in video player"); - throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player"); - } - - @Override - public void setPlaybackPitch(float f) { - Log.e(TAG, "Setting playback pitch unsupported in video player"); - throw new UnsupportedOperationException("Setting playback pitch unsupported in video player"); - } - - @Override - public void setPlaybackSpeed(float f) { - Log.e(TAG, "Setting playback speed unsupported in video player"); - throw new UnsupportedOperationException("Setting playback speed unsupported in video player"); - } - - @Override - public void setVideoScalingMode(int mode) { - super.setVideoScalingMode(mode); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java b/app/src/main/java/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java deleted file mode 100644 index ac38ec876..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/syndication/FeedDiscoverer.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.danoeh.antennapod.util.syndication; - -import android.net.Uri; -import org.apache.commons.lang3.StringUtils; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.io.File; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here: - *

- * http://www.rssboard.org/rss-autodiscovery - *

- * http://blog.whatwg.org/feed-autodiscovery - */ -public class FeedDiscoverer { - - private static final String MIME_RSS = "application/rss+xml"; - private static final String MIME_ATOM = "application/atom+xml"; - - /** - * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. - * - * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if - * a title cannot be found). - */ - public Map findLinks(File in, String baseUrl) throws IOException { - return findLinks(Jsoup.parse(in, null), baseUrl); - } - - /** - * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. - * - * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if - * a title cannot be found). - */ - public Map findLinks(String in, String baseUrl) throws IOException { - return findLinks(Jsoup.parse(in), baseUrl); - } - - private Map findLinks(Document document, String baseUrl) { - Map res = new LinkedHashMap(); - Elements links = document.head().getElementsByTag("link"); - for (Element link : links) { - String rel = link.attr("rel"); - String href = link.attr("href"); - if (!StringUtils.isEmpty(href) && - (rel.equals("alternate") || rel.equals("feed"))) { - String type = link.attr("type"); - if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) { - String title = link.attr("title"); - String processedUrl = processURL(baseUrl, href); - if (processedUrl != null) { - res.put(processedUrl, - (StringUtils.isEmpty(title)) ? href : title); - } - } - } - } - return res; - } - - private String processURL(String baseUrl, String strUrl) { - Uri uri = Uri.parse(strUrl); - if (uri.isRelative()) { - Uri res = Uri.parse(baseUrl).buildUpon().path(strUrl).build(); - return (res != null) ? res.toString() : null; - } else { - return strUrl; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java b/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java deleted file mode 100644 index 767034ed2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java +++ /dev/null @@ -1,81 +0,0 @@ -package de.danoeh.antennapod.util.vorbiscommentreader; - -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; - -public class OggInputStream extends InputStream { - private InputStream input; - - /** True if OggInputStream is currently inside an Ogg page. */ - private boolean isInPage; - private long bytesLeft; - - public OggInputStream(InputStream input) { - super(); - isInPage = false; - this.input = input; - } - - @Override - public int read() throws IOException { - if (!isInPage) { - readOggPage(); - } - - if (isInPage && bytesLeft > 0) { - int result = input.read(); - bytesLeft -= 1; - if (bytesLeft == 0) { - isInPage = false; - } - return result; - } - return -1; - } - - private void readOggPage() throws IOException { - // find OggS - int[] buffer = new int[4]; - int c = 0; - boolean isInOggS = false; - while ((c = input.read()) != -1) { - switch (c) { - case 'O': - isInOggS = true; - buffer[0] = c; - break; - case 'g': - if (buffer[1] != c) { - buffer[1] = c; - } else { - buffer[2] = c; - } - break; - case 'S': - buffer[3] = c; - break; - default: - if (isInOggS) { - Arrays.fill(buffer, 0); - isInOggS = false; - } - } - if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g' - && buffer[3] == 'S') { - break; - } - } - // read segments - IOUtils.skipFully(input, 22); - bytesLeft = 0; - int numSegments = input.read(); - for (int i = 0; i < numSegments; i++) { - bytesLeft += input.read(); - } - isInPage = true; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java b/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java deleted file mode 100644 index b2f149ddd..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java +++ /dev/null @@ -1,101 +0,0 @@ -package de.danoeh.antennapod.util.vorbiscommentreader; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.VorbisCommentChapter; - -import java.util.ArrayList; -import java.util.List; - -public class VorbisCommentChapterReader extends VorbisCommentReader { - private static final String TAG = "VorbisCommentChapterReader"; - - private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*"; - private static final String CHAPTER_ATTRIBUTE_TITLE = "name"; - private static final String CHAPTER_ATTRIBUTE_LINK = "url"; - - private List chapters; - - public VorbisCommentChapterReader() { - } - - @Override - public void onVorbisCommentFound() { - System.out.println("Vorbis comment found"); - } - - @Override - public void onVorbisCommentHeaderFound(VorbisCommentHeader header) { - chapters = new ArrayList(); - System.out.println(header.toString()); - } - - @Override - public boolean onContentVectorKey(String content) { - return content.matches(CHAPTER_KEY); - } - - @Override - public void onContentVectorValue(String key, String value) - throws VorbisCommentReaderException { - if (BuildConfig.DEBUG) - Log.d(TAG, "Key: " + key + ", value: " + value); - String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key); - int id = VorbisCommentChapter.getIDFromKey(key); - Chapter chapter = getChapterById(id); - if (attribute == null) { - if (getChapterById(id) == null) { - // new chapter - long start = VorbisCommentChapter.getStartTimeFromValue(value); - chapter = new VorbisCommentChapter(id); - chapter.setStart(start); - chapters.add(chapter); - } else { - throw new VorbisCommentReaderException( - "Found chapter with duplicate ID (" + key + ", " - + value + ")"); - } - } else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) { - if (chapter != null) { - chapter.setTitle(value); - } - } else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) { - if (chapter != null) { - chapter.setLink(value); - } - } - } - - @Override - public void onNoVorbisCommentFound() { - System.out.println("No vorbis comment found"); - } - - @Override - public void onEndOfComment() { - System.out.println("End of comment"); - for (Chapter c : chapters) { - System.out.println(c.toString()); - } - } - - @Override - public void onError(VorbisCommentReaderException exception) { - exception.printStackTrace(); - } - - private Chapter getChapterById(long id) { - for (Chapter c : chapters) { - if (((VorbisCommentChapter) c).getVorbisCommentId() == id) { - return c; - } - } - return null; - } - - public List getChapters() { - return chapters; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java b/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java deleted file mode 100644 index 8c47393c9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.danoeh.antennapod.util.vorbiscommentreader; -public class VorbisCommentHeader { - private String vendorString; - private long userCommentLength; - - public VorbisCommentHeader(String vendorString, long userCommentLength) { - super(); - this.vendorString = vendorString; - this.userCommentLength = userCommentLength; - } - - @Override - public String toString() { - return "VorbisCommentHeader [vendorString=" + vendorString - + ", userCommentLength=" + userCommentLength + "]"; - } - - public String getVendorString() { - return vendorString; - } - - public long getUserCommentLength() { - return userCommentLength; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java b/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java deleted file mode 100644 index 718a4f30f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java +++ /dev/null @@ -1,194 +0,0 @@ -package de.danoeh.antennapod.util.vorbiscommentreader; - -import org.apache.commons.io.EndianUtils; -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; - - -public abstract class VorbisCommentReader { - /** Length of first page in an ogg file in bytes. */ - private static final int FIRST_PAGE_LENGTH = 58; - private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024; - private static final int PACKET_TYPE_IDENTIFICATION = 1; - private static final int PACKET_TYPE_COMMENT = 3; - - /** Called when Reader finds identification header. */ - public abstract void onVorbisCommentFound(); - - public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header); - - /** - * Is called every time the Reader finds a content vector. The handler - * should return true if it wants to handle the content vector. - */ - public abstract boolean onContentVectorKey(String content); - - /** - * Is called if onContentVectorKey returned true for the key. - * - * @throws VorbisCommentReaderException - */ - public abstract void onContentVectorValue(String key, String value) - throws VorbisCommentReaderException; - - public abstract void onNoVorbisCommentFound(); - - public abstract void onEndOfComment(); - - public abstract void onError(VorbisCommentReaderException exception); - - public void readInputStream(InputStream input) - throws VorbisCommentReaderException { - try { - // look for identification header - if (findIdentificationHeader(input)) { - - onVorbisCommentFound(); - input = new OggInputStream(input); - if (findCommentHeader(input)) { - VorbisCommentHeader commentHeader = readCommentHeader(input); - if (commentHeader != null) { - onVorbisCommentHeaderFound(commentHeader); - for (int i = 0; i < commentHeader - .getUserCommentLength(); i++) { - try { - long vectorLength = EndianUtils - .readSwappedUnsignedInteger(input); - String key = readContentVectorKey(input, - vectorLength).toLowerCase(); - boolean readValue = onContentVectorKey(key); - if (readValue) { - String value = readUTF8String( - input, - (int) (vectorLength - key.length() - 1)); - onContentVectorValue(key, value); - } else { - IOUtils.skipFully(input, - vectorLength - key.length() - 1); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - onEndOfComment(); - } - - } else { - onError(new VorbisCommentReaderException( - "No comment header found")); - } - } else { - onNoVorbisCommentFound(); - } - } catch (IOException e) { - onError(new VorbisCommentReaderException(e)); - } - } - - private String readUTF8String(InputStream input, long length) - throws IOException { - byte[] buffer = new byte[(int) length]; - - IOUtils.readFully(input, buffer); - Charset charset = Charset.forName("UTF-8"); - return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString(); - } - - /** - * Looks for an identification header in the first page of the file. If an - * identification header is found, it will be skipped completely and the - * method will return true, otherwise false. - * - * @throws IOException - */ - private boolean findIdentificationHeader(InputStream input) - throws IOException { - byte[] buffer = new byte[FIRST_PAGE_LENGTH]; - IOUtils.readFully(input, buffer); - int i; - for (i = 6; i < buffer.length; i++) { - if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o' - && buffer[i - 3] == 'r' && buffer[i - 2] == 'b' - && buffer[i - 1] == 'i' && buffer[i] == 's' - && buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) { - return true; - } - } - return false; - } - - private boolean findCommentHeader(InputStream input) throws IOException { - char[] buffer = new char["vorbis".length() + 1]; - for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) { - char c = (char) input.read(); - int dest = -1; - switch (c) { - case PACKET_TYPE_COMMENT: - dest = 0; - break; - case 'v': - dest = 1; - break; - case 'o': - dest = 2; - break; - case 'r': - dest = 3; - break; - case 'b': - dest = 4; - break; - case 'i': - dest = 5; - break; - case 's': - dest = 6; - break; - } - if (dest >= 0) { - buffer[dest] = c; - if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r' - && buffer[4] == 'b' && buffer[5] == 'i' - && buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) { - return true; - } - } else { - Arrays.fill(buffer, (char) 0); - } - } - return false; - } - - private VorbisCommentHeader readCommentHeader(InputStream input) - throws IOException, VorbisCommentReaderException { - try { - long vendorLength = EndianUtils.readSwappedUnsignedInteger(input); - String vendorName = readUTF8String(input, vendorLength); - long userCommentLength = EndianUtils - .readSwappedUnsignedInteger(input); - return new VorbisCommentHeader(vendorName, userCommentLength); - } catch (UnsupportedEncodingException e) { - throw new VorbisCommentReaderException(e); - } - } - - private String readContentVectorKey(InputStream input, long vectorLength) - throws IOException { - StringBuffer buffer = new StringBuffer(); - for (int i = 0; i < vectorLength; i++) { - char c = (char) input.read(); - if (c == '=') { - return buffer.toString(); - } else { - buffer.append(c); - } - } - return null; // no key found - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java b/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java deleted file mode 100644 index 574373241..000000000 --- a/app/src/main/java/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danoeh.antennapod.util.vorbiscommentreader; -public class VorbisCommentReaderException extends Exception { - - public VorbisCommentReaderException() { - super(); - // TODO Auto-generated constructor stub - } - - public VorbisCommentReaderException(String arg0, Throwable arg1) { - super(arg0, arg1); - // TODO Auto-generated constructor stub - } - - public VorbisCommentReaderException(String arg0) { - super(arg0); - // TODO Auto-generated constructor stub - } - - public VorbisCommentReaderException(Throwable arg0) { - super(arg0); - // TODO Auto-generated constructor stub - } - -} -- cgit v1.2.3